Skip to content

Security 6 + Spring Boot 3.0 + JWT

Spring Security 6 + Spring Boot 3.0 + JWT

大概流程

  • 1,拦截未登录和token失效--->实现AuthenticationEntryPoint
  • 2,拦截未授权的请求--->实现AccessDeniedHandler
  • 3,修改配置类--->配置MySecurityConfig
  • 4,用户增删改查--->完成sysUser CRUD
  • 5,完成认证管理器--->配置 AuthenticationManager
  • 6,编写登录和登出--->实现login,logout方法
  • 7,自定义用户信息类--->实现UserDetails
  • 8,实现根据username查询用户,从数据库中验证用户名密码--->实现UserDetailsService
  • 9,配置密码加密方式--->配置PasswordEncoder
  • 10,配置jwt过滤器--->实现OncePerRequestFilter接口

1,引入依赖

xml
<!--        解决jwt java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter 错误-->
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.1</version>
        </dependency>
<!--        jwt-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
<!--        spring security-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
<!--        hutool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.4</version>
        </dependency>

2,配置jwt

jwt

  • 配置jwt请查看文档 jwt
  • 1, 编写jwt工具类
  • 2, 配置jwt

3,拦截未登录和token失效

TIP

  • AccessDeniedHandler接口,当权限不足时,我们需要设置权限不足状态码403,并将错误信息返回给前端
  • AuthenticationEntryPoint接口,当认证失败时,返回401状态码,并返回给前端
java

// MySecurityAuthenticationHandler.java
@Component
public class MySecurityAuthenticationHandler implements AccessDeniedHandler, AuthenticationEntryPoint {
    /**
     * 未登录或者token失效时访问接口时,自定义的返回结果
     * AuthenticationEntryPoint
     * @param request 请求
     * @param response 响应
     * @param authException 权限异常
     * @throws IOException IO异常
     * @throws ServletException Servlet异常
     */
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        System.out.println("request = " + authException.getMessage());
        AjaxResponse ajaxResponse = AjaxResponse.error(new CustomException(CustomExceptionType.USER_UNAUTHORIZED));
        ObjectMapper mapper = new ObjectMapper();
        String errorString = mapper.writeValueAsString(ajaxResponse);
        WebUtil.renderString(response, errorString);
    }

    /**
     * 无权限访问接口时,返回403结果
     * AccessDeniedHandler
     * @param request 请求
     * @param response 响应
     * @param accessDeniedException 权限异常
     * @throws IOException IO异常
     * @throws ServletException Servlet异常
     */
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        System.out.println("request = " + accessDeniedException.getMessage());
        AjaxResponse ajaxResponse = AjaxResponse.error(new CustomException(CustomExceptionType.USER_ACCESS_DENIED));
        ObjectMapper mapper = new ObjectMapper();
        String errorString = mapper.writeValueAsString(ajaxResponse);
        WebUtil.renderString(response, errorString);
    }

}

4,修改配置类

security配置类

  • 配置白名单
  • 配置密码加密方式
  • 配置AuthenticationManager
  • 配置错误处理器
java
// MySecurityConfig.java
@Configuration
public class MySecurityConfig  {

    /**
     * 白名单
     */
    public static final List<String> API_WHITE_LIST = List.of("/hello","/testJwt",
            "/sysUser/login","/sysUser/logout",
            "/swagger-ui.html","/swagger-ui/**","/swagger-resources/**","/v3/api-docs/**","/webjars/**",
            "/static/**","/resources/**");

    /**
     * 配置密码加密方式
     * @return PasswordEncoder
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


    /**
     * 配置AuthenticationManager
     * @param authenticationConfiguration AuthenticationConfiguration
     * @return AuthenticationManager
     * @throws Exception 异常
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    /**
     * 配置拦截规则
     * @param http httpSecurity
     * @param mySecurityAuthenticationHandler 自定义的权限处理器
     * @return SecurityFilterChain
     * @throws Exception 异常
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http,MySecurityAuthenticationHandler mySecurityAuthenticationHandler) throws Exception {
        //权限配置
        http.authorizeHttpRequests(auth->{
            try {
                auth.requestMatchers(API_WHITE_LIST.toArray(new String[0])).permitAll()
                        .anyRequest().authenticated().and().csrf(AbstractHttpConfigurer::disable);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });
       // 错误处理
       http.exceptionHandling()
               // 注册未登录或者token失效时访问接口时,自定义的返回结果
               .accessDeniedHandler(mySecurityAuthenticationHandler)
               // 注册无权限访问接口时,自定义的返回结果
               .authenticationEntryPoint(mySecurityAuthenticationHandler);
       // 允许跨域
       http.cors();
       return http.build();
    }
}

5,用户增删改查

TIP

使用mybatis-plus 生成的代码,此处不再赘述

  • 1,创建sysUser实体类
  • 2,创建sysUserMapper
  • 3,创建sysUserService
  • 4,创建sysUserController

6, 配置AuthenticationManager

java

// MySecurityConfig.java
@Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

7,编写用户登录和登出接口

java
// SysUserServiceImpl.java

@Override
public String login(SysUser sysUser) {
    // 1,使用AuthenticationManager类的 authenticate方法进行认证,如果认证成功,返回一个Authentication对象,否则返回null
    UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(sysUser.getUserName(), sysUser.getPassword());
    Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
    // 2, 如果没通过抛出异常
    if(Objects.isNull(authenticate)){
        throw new CustomException(CustomExceptionType.USER_LOGIN_EXCEPTION);
    }
    MyUserDetails UserDetail = (MyUserDetails) authenticate.getPrincipal();
    String UserId = UserDetail.getUser().getUserId().toString();
    // 3,如果通过使用jwt生成token,把token返回给前端
    String jwt = jwtUtil.createJwt(UserId);
    // 4,redis中存储token,userid作为key
    redisCache.setCacheObject(Constant.REDIS_LOGIN_KEY +UserId, UserDetail);
    return jwt;
}
@Override
public String logout() {
    // 1,获取当前登录用户认证信息
    UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
    // 2,获取当前登录用户信息
    MyUserDetails loginUser = (MyUserDetails) authentication.getPrincipal();
    // 3,获取当前登录用户id
    Long id = loginUser.getUser().getUserId();
    // 4,删除redis中的token
    redisCache.deleteObject(Constant.REDIS_LOGIN_KEY +id);
    // 5,清除当前登录用户认证信息
    SecurityContextHolder.clearContext();
    return "登出成功";
}

8,自定义用户信息类,实现UserDetails接口

java
@Data
@NoArgsConstructor
public class MyUserDetails implements UserDetails {

    private SysUser user;

    private List<String> permissions;

    @JsonIgnore
    List<SimpleGrantedAuthority> authorities;

    /**
     * 构造方法
     * @param user 用户信息
     * @param permissions 权限集合
     */
    public MyUserDetails(SysUser user, List<String> permissions) {
        this.user = user;
        this.permissions = permissions;
    }

    /**
     * 获取用户权限
     * @return 权限集合
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if(authorities == null){
            authorities = permissions.stream()
                    .map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toList());
        }
        return authorities;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUserName();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

9,实现UserDetailsService接口,查询用户信息

java
@Service
@Slf4j
public class MyUserDetailServiceImpl implements UserDetailsService {

    @Resource
    SysUserDao sysUserDao;
    private List<String> permissions;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 查询用户信息
        SysUser sysUser = sysUserDao.SelectUserByName(username);
        log.info("用户信息:{}", sysUser);
        // 判断用户是否存在
        if (Objects.isNull(sysUser)) {
            throw new RuntimeException("用户不存在");
        }
//        查询用户权限
//        List<String> permissions = sysUserService.getPermissionsByUserId(sysUser.getUserId());
        List<String> permissions = new ArrayList<>();
        permissions.add("**");
        return new MyUserDetails(sysUser, permissions);
    }
}

10,配置密码加密方式

java
// MySecurityConfig.java
@Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

11,配置拦截规则

java
//JwtAuthenticationTokenFilter.java

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Resource
    private JwtUtil jwtUtil;

    @Resource
    private RedisCache redisCache;
    /**
     * jwt请求过滤器
     * @param request 请求
     * @param response 响应
     * @param filterChain 过滤器链
     * @throws ServletException 异常
     * @throws IOException 异常
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
//        1,获取请求头中的token
        String token = request.getHeader("Authorization");
//        2,判断token是否为空,是否以Bearer开头
        if(!StringUtils.hasText(token) ||  !token.startsWith("Bearer ")){
            filterChain.doFilter(request, response);
            return;
        }
//        3,解析token
        String userId;
        try {
            Claims claims = jwtUtil.parseJwt(token.substring(7));
            userId = claims.getSubject();
        } catch (Exception e) {
            throw new RuntimeException("令牌不正确");
        }
//        4,从redis 获取用户信息
        MyUserDetails myUserDetails = redisCache.getCacheObject(Constant.REDIS_LOGIN_KEY + userId);
        if(Objects.isNull(myUserDetails)){
            throw new RuntimeException("用户不存在");
        }
        System.out.println("用户验证通过:" + myUserDetails);
//        5,构造UsernamePasswordAuthenticationToken对象,并设置到SecurityContextHolder中
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(myUserDetails, null, myUserDetails.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
//        6,放行
        filterChain.doFilter(request, response);
    }
}

12,配置拦截器

java
// MySecurityConfig.java

@Configuration
public class MySecurityConfig  {

    @Resource
    private JwtAuthenticationTokenFilter jwtAuthenticationFilter;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http,MySecurityAuthenticationHandler mySecurityAuthenticationHandler) throws Exception {
    
    ...
    // jwt过滤器
    http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    // 允许跨域
    http.cors();
    return http.build();
    }

}

杨利伟的博客