Spring Security JWT Redis 认证授权

624 阅读3分钟

概要

Spring Security 是一个认证授权的框架,记录配置整合过程和遇到的一些问题。(一种常规的配置方案,不展开描述 jwt 和 Redis, ) spring boot 2.7.7

整合步骤

首先来看一下默认状态下 Security 框架的Filter顺序

WeChat23f9f46d5d881f375b0086dd4b77115b.png

配置后的的过滤器

WeChatbbd5649e139b5a53a3b53864f3ad807d.png

依赖库

 <!-- security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  <!-- jwt -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
</dependency>
<!-- redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

增加 UserDetails 实现类 SecurityUserDetailsBo

提供给 Security 的 UserDetails

@Data
@NoArgsConstructor
public class SecurityUserDetailsBo implements UserDetails {

    // 接收数据返回的用户 PO ,login 过程中在 loadUserByUsername 中设置
    private UmsUserPo umsUserPo;

    // 接收数据返回的权限 PoList ,login 过程中在 loadUserByUsername 中设置
    private List<UmsPermissionPo> umsPermissionPoList;


    // 提供给 Security 鉴权, @JsonIgnore:在redis序列化中忽略
    @JsonIgnore
    private List<SimpleGrantedAuthority> authorities;


    public SecurityUserDetailsBo(UmsUserPo umsUserPo, List<UmsPermissionPo> umsPermissionPoList) {
        this.umsUserPo = umsUserPo;
        this.umsPermissionPoList = umsPermissionPoList;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {

        // jwt 鉴权过中,如果已经存在则会直接反,这里是一个优化,一个线程中你只登录的第一次需要
        if (Objects.nonNull(authorities)){
            return  authorities;
        }

        // umsPermissionPoList => authorities
        return umsPermissionPoList.stream()
                .map(role ->new SimpleGrantedAuthority(role.getPermission()))
                .collect(Collectors.toList());

    }

    // 返回登录用户账户名和密码 用于Security判断
    @Override
    public String getPassword() {
        return umsUserPo.getPassword();
    }

    @Override
    public String getUsername() {
        return umsUserPo.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;
    }
}

增加 UserDetailsService 实现类 UmsAdminServiceImpl

@Service
public class UmsAdminServiceImpl extends ServiceImpl<UmsAdminMapper, UmsUserPo> implements UmsAdminService, UserDetailsService {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private UmsPermissionMapper umsPermissionMapper;

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private RedisUtil redisUtil;

    @Value("${redis.database}")
    private String REDIS_DATABASE;

    @Value("${redis.expire}")
    private Long REDIS_EXPIRE;

    @Override
    public String login(UmsAdminLoginDto umsAdminLoginDto) {

        // 使用 Security 中的UsernamePasswordAuthenticationToken -> authenticationToken
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(umsAdminLoginDto.getUserName(),umsAdminLoginDto.getPassword());
        // 在SecurityConfig 中配置 AuthenticationManager 会调用 loadUserByUsername
        Authentication authentication =  authenticationManager.authenticate(authenticationToken);

        SecurityUserDetailsBo securityUserDetailsBo = (SecurityUserDetailsBo) authentication.getPrincipal();
        String userFlag = securityUserDetailsBo.getUsername().toString();
        // 生成token
        String userToken = jwtUtil.generateToken(userFlag);

        String key = REDIS_DATABASE  + ":" + securityUserDetailsBo.getUsername();

        // token 存储入 redis
        redisUtil.set(key,securityUserDetailsBo,REDIS_EXPIRE);

        return userToken;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        LambdaQueryWrapper<UmsUserPo> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(UmsUserPo::getUserName,username);
        UmsUserPo umsUserPo  =  this.getOne(queryWrapper);
        // 如果用户不存在
        if(Objects.isNull(umsUserPo)){
            throw new UsernameNotFoundException("用户不存在");
        }
        // 查询权限
        List<UmsPermissionPo> permissionList = umsPermissionMapper.getPermissionList(umsUserPo.getUserId());

        return new SecurityUserDetailsBo(umsUserPo,permissionList);


    }


}

添加过滤器 JwtAuthenticationTokenFilter

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private RestAuthenticationEntryPoint restAuthenticationEntryPoint;
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private JwtUtil jwtUtil;
    @Value("${jwt.toKenHeader}")
    private String tokenHeader;
    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @Value("${redis.database}")
    private String REDIS_DATABASE;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
     try {
         String authHeader = request.getHeader(this.tokenHeader);
         if(StringUtils.hasText(authHeader) && authHeader.startsWith(this.tokenHead)){
             String authToken = authHeader.substring(this.tokenHead.length()).trim();
             // 获取jwt中的username
             String username = jwtUtil.getUserNameFromToken(authToken);

             if ( !Objects.isNull(username) && SecurityContextHolder.getContext().getAuthentication() == null){
                 // 检验 jwt 是否生效
                 if(jwtUtil.validateToken(authToken,username)){
                     // 通过 jwt 中的username 拼接redis key 获取用户信息
                     String key = REDIS_DATABASE  + ":" + username;

                     SecurityUserDetailsBo securityUserDetailsBo = (SecurityUserDetailsBo) redisUtil.get(key);
                     //  从 redis 中获取数据
                     if(!Objects.isNull(securityUserDetailsBo)  ){
                         UsernamePasswordAuthenticationToken authenticationToken =
                                 new UsernamePasswordAuthenticationToken(securityUserDetailsBo,null,securityUserDetailsBo.getAuthorities());

                         SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                     }else {
                         throw new ExpiredJwtException("redis 未查询到");
                     }
                 } else {
                     throw new ExpiredJwtException("token失效或异常");
                 }
             }else {
                 throw new ExpiredJwtException("header中的token异常");
             }
         }
         filterChain.doFilter(request,response);
     }catch (AuthenticationException ex){
         // 目前得知 doFilterInternal 中抛出异常的最佳方式, AuthenticationEntryPoint 可以获得 ex.message,
         // message 可以设置成 ResultCode,从而在AuthenticationEntryPoint中枚举 异常message
         SecurityContextHolder.clearContext();
         this.restAuthenticationEntryPoint.commence(request, response, ex);
     }
    }
}

配置 SecurityConfig

@Configuration
// 开启权限
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {

    @Autowired
    private RestAuthenticationEntryPoint restAuthenticationEntryPoint;
    @Autowired
    private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    @Bean
    AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();

    }


    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // 关闭 csrf
            .csrf().disable()
            // 关闭 session
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                
                // 添加认证和授权异常回调
                .exceptionHandling()
                .accessDeniedHandler(restfulAccessDeniedHandler)
                .authenticationEntryPoint(restAuthenticationEntryPoint)
                .and()
                .authorizeRequests()
                .antMatchers("/admin/login").anonymous()

                // swagger knife4j资源放行
                .antMatchers("/favicon.ico").anonymous()
                .antMatchers("/swagger-ui/**").anonymous()
                .antMatchers("/swagger-resources/**").anonymous()
                .antMatchers("/profile/**").anonymous()
                .antMatchers("/profile/**").anonymous()
                .antMatchers("/v3/**").anonymous()

                .antMatchers("/doc.html").anonymous()
                .antMatchers("/webjars/**").anonymous()
                .anyRequest().authenticated()
                .and()
                // 添加jwt 顾虑器
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

配置授权

@ApiOperation(value = "管理菜单")
@RequestMapping(value = "/menu01", method = RequestMethod.GET)
@PreAuthorize("hasAuthority('admin:menu:one')")
@ResponseBody
public CommonResult menu01() {

    return CommonResult.success("管理菜单");

}
@ApiOperation(value = "员工菜单")
@RequestMapping(value = "/menu02", method = RequestMethod.GET)
@PreAuthorize("hasAuthority('employee:menu:one')")
@ResponseBody
public CommonResult menu02() {

    return CommonResult.success("员工菜单");

}

通过 PreAuthorize 配置授权即可,hasAuthority 实际执行逻辑,如果想封装自己的权限信息
如下:

// 注入到容器
@Component("sp")
public class SecurityPermission {

    public boolean hasAuthority(){
        // 需要的鉴权逻辑
        // ....
        return true;
    }
}

// 使用
@PreAuthorize("@sp.hasAuthority('employee:menu:one')")
public CommonResult menu02() {
    return CommonResult.success("员工菜单");

}

其他方案

# 别再用过时的方式了!全新版本Spring Security,这样用才够优雅!
# 仅需四步,整合SpringSecurity+JWT实现登录认证 !

SpringBoot实战电商项目mall(50k+sta