Spring Security+JWT学习笔记

这篇博客介绍了如何将SpringSecurity与JWT结合,用于实现无状态的认证授权。作者首先解释了JWT的概念,然后展示了如何创建JWT工具类,包括生成和解析JWT。接着,详细说明了自定义SpringSecurity配置,包括禁用Session、编写自定义认证过滤器以及授权过滤器。此外,还涉及了RefreshToken的处理和异常处理。最后,提到了在实际开发中遇到的一些问题及其解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Spring Security + JWT 认证学习

  1. JWT是什么
  2. Spring Security文档
  3. youtobe上的一个教学视频
  4. 知乎上一篇Spring Security+JWT

之前学习认证授权相关知识,先了解学习了shiro,最近在学习Spring Security,同时了解到分布式认证方式JWT,所以尝试结合两者写一段demo,算是同时加深对两者的理解和认识。

JWT工具类

封装JwtUtils.

public final class JwtUtils {

    private final static String SECRET_KEY = "flyzzfighting";

    
	//生成jwt token    
    public static String generate(Map<String,Object> map,Duration duration) {
        Date expiryDate = new Date(System.currentTimeMillis()+duration.toMillis());
        return Jwts.builder().setSubject("token").setClaims(map).setIssuedAt(new Date()).setExpiration(expiryDate)
                .signWith(SignatureAlgorithm.HS512,SECRET_KEY).compact();
    }

    
    //解析jwt token
    public static Claims parse(String token) {
        if(!StringUtils.hasLength(token)) {
            return null;
        }

        Claims claims = null;

        try {
            claims = Jwts.parser()
                    .setSigningKey(SECRET_KEY)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (JwtException e) {
            log.info(e.toString());
        }
        return claims;
    }
}

这样封装只是为了获取token,对于JWT解析可能产生各种异常,如超时,签名错误等没有分开处理。

Spring Security配置

自定义配置类

一般定义如下类,可以对Spring Security进行配置。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    //禁用Session
    http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}

Authenciation 认证

自定义认证类,可以参考UsernamePasswordAuthenticationFilter进行编写,这里继承UsernamePassword的父类重写,在attemptAuthentication方法中重写认证逻辑,之后在successfulAuthentication方法中编写认证成功后的逻辑,这里是在响应头中添加token,不过写法有点烂,主要是对java 相关更优API不太熟悉,以及对jackson对于List容器的序列化不太熟悉,之后有待学习,这里在access_token中存入了userName和该用户的权限authority,refresh_token中存入了userName。另外,在request域中放入userName用于告诉Controller已认证,并可以通过userName查询数据库,处理后返回相应DTO,最后配好SecurityContext中的Authentication。

public class CustomAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/api/login", "POST");

    private AuthenticationManager authenticationManager;

    protected CustomAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
        super.setAuthenticationManager(authenticationManager );
    }


    static class UserInfo{
        public String userName;
        public String password;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {


        if(!"POST".equals(request.getMethod())) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {

            ObjectMapper objectMapper = new ObjectMapper();
            UserInfo userInfo = null;
            try {
                userInfo = objectMapper.readValue(request.getInputStream(),UserInfo.class);
            } catch (IOException e) {
                e.printStackTrace();
            }
            assert userInfo != null;
            if(userInfo.userName==null) {
                throw new AuthenticationServiceException("用户名不能为空");
            } else {
                userInfo.userName = userInfo.userName.trim();
                userInfo.password = userInfo.password.trim();
                return super.getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(userInfo.userName,userInfo.password));
            }
        }
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        Map<String,Object> accessMap = new HashMap<>();
        Map<String,Object> refreshMap = new HashMap<>();
        User user = (User) authResult.getPrincipal();
        accessMap.put("userName",user.getUsername());
        refreshMap.put("userName",user.getUsername());
        accessMap.put("authority",authResult.getAuthorities());

        request.setAttribute("userName", user.getUsername());
        response.setHeader("accessToken",JwtUtils.generate(accessMap,Duration.ofMinutes(3)));
        response.setHeader("refreshToken",JwtUtils.generate(refreshMap,Duration.ofMinutes(9)));
        SecurityContextHolder.getContext().setAuthentication(authResult);
        chain.doFilter(request,response);
    }

}

重写后,需要用它替代默认的UsernamePasswordAuthentication,在配置类中配置:

 http.addFilterAt(new CustomAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
 

这样装配的话,其中的AuthenticationManager是需要自己注入的,于是写了一个带参构造方法传入,同时还要为AuthenticationManager配置UserDetailsServicePasswordEncoder

@Component
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    UserService userService;

    @Autowired
    AuthorityService authorityService;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        UserDO userDO = userService.loadUserByName(s);
        if(userDO!=null) {
            List<AuthorityDO> list = authorityService.getAuthorityListById(userDO.getId());
            List<GrantedAuthority> list1 = new ArrayList<>();
            for (AuthorityDO authorityDO:list) {
                list1.add(new SimpleGrantedAuthority(authorityDO.getName()));
            }
            return new User(userDO.getUserName(),userDO.getPassword(),list1);
        } else {
            throw new UsernameNotFoundException("用户名不存在");
        }
    }
}


//配置类中
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

在配置类中为AuthenticationManager配置:

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

	//在构造方法中传参
    @Override
    public AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }
	
	//加入到对应configure中
    http.addFilterAt(new CustomAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);

认证完成

Authorization 授权

编写相关Filter,777的前缀是为了不让它拦截到refresh_token,所以发请求时如果是access_token要带上777前缀。大概就是解析token,得到用户名和相关权限,装到SecurityContext中。

public class JwtTokenFilter extends OncePerRequestFilter {

    private final String MY_SIGN = "777 ";

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {

        Claims claims = null;
        if(httpServletRequest.getHeader("Authorization")!=null&&httpServletRequest.getHeader("Authorization").startsWith(MY_SIGN)) {
            claims = JwtUtils.parse(httpServletRequest.getHeader("Authorization").substring(MY_SIGN.length()));
        }
        if(!ObjectUtils.isEmpty(claims)) {
            String userName = claims.get("userName",String.class);
            List list = claims.get("authority", List.class);
            List<SimpleGrantedAuthority> authorityList = new ArrayList<>();
            for (Object o : list) {
                authorityList.add(new SimpleGrantedAuthority((String) ((LinkedHashMap) o).get("authority")));
            }
            UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userName,"",authorityList);
            SecurityContextHolder.getContext().setAuthentication(token);
        }
        filterChain.doFilter(httpServletRequest,httpServletResponse);
    }
}

之后把过滤器加到AnonymousAuthenticationFilter前:

http.addFilterBefore(new JwtTokenFilter(), AnonymousAuthenticationFilter.class);

Refresh Token

大概就是检查token,合法就重新发放token. 通过refresh_token中的userName查询数据库,重新签名,发放JWT token。

    @GetMapping("/refreshtoken")
    public String refresh(@RequestHeader(value = "Authorization",required = false) String token, HttpServletResponse response) {
        if(token==null) {
            return "请重新登录";
        } else {
            Claims claims = JwtUtils.parse(token);
            if(claims==null) {
                return "请重新登录";
            }
            String userName = claims.get("userName",String.class);
            UserDO userDO = userService.loadUserByName(userName);
            List<AuthorityDO> list = authorityService.getAuthorityListById(userDO.getId());
            List<GrantedAuthority> list1 = new ArrayList<>();
            for (AuthorityDO authorityDO:list) {
                list1.add(new SimpleGrantedAuthority(authorityDO.getName()));
            }
            Map<String,Object> accessMap = new HashMap<>();
            Map<String,Object> refreshMap = new HashMap<>();
            accessMap.put("userName",userName);
            refreshMap.put("userName",userName);
            accessMap.put("authority",list1);
            response.setHeader("accessToken",JwtUtils.generate(accessMap, Duration.ofMinutes(30)));
            response.setHeader("refreshToken",JwtUtils.generate(refreshMap,Duration.ofMinutes(90)));
            return "刷新成功";
        }
    }

简单异常处理

对于认证或权限失败,可以编写处理类实现AuthenticationEntryPointAccessDeniedHandler接口,并配置。

        http.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint());
        http.exceptionHandling().accessDeniedHandler(new CustomAccessDenyEntryPoint());

Configure完整

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        http.addFilterAt(new CustomAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
        http.addFilterBefore(new JwtTokenFilter(), AnonymousAuthenticationFilter.class);
        http.authorizeRequests().antMatchers("/api/login","/api/refreshtoken").permitAll();
        http.authorizeRequests().anyRequest().authenticated();
        http.csrf().disable();
        http.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint());
        http.exceptionHandling().accessDeniedHandler(new CustomAccessDenyEntryPoint());
    }

安全注解的使用

在配置类上使用@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)注解。

    @PreAuthorize("hasAuthority(\"HELLO\")")
    @Secured({"ROLE_ADMIN"})

@Secured:只能检查role,即要有前缀ROLE_

杂记

  1. 连接数据库时出现异常,提示Public Key Retrival错误。

    解决方案

  2. 使用java11,mybatis,由于java11移除了了javaEE 中xml支持,会报错。

    添加依赖:

    //我只添加了第一个依赖就行了,stackoverflow上面给出的是这三个依赖。
    <dependency>
      <groupId>javax.xml.bind</groupId>
      <artifactId>jaxb-api</artifactId>
      <version>2.3.0</version>
    </dependency>
    <dependency>
      <groupId>com.sun.xml.bind</groupId>
      <artifactId>jaxb-core</artifactId>
      <version>2.3.0</version>
    </dependency>
    <dependency>
      <groupId>com.sun.xml.bind</groupId>
      <artifactId>jaxb-impl</artifactId>
      <version>2.3.0</version>
    </dependency>
    
  3. github地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值