Springboot集成Spring Security实现JWT认证的步骤详解

作者:南瓜慢说 时间:2021-09-18 13:18:08 

目录
  • 1 简介

  • 2 项目整合

    • 2.1 JWT整合

      • 2.1.1 JWT工具类

      • 2.1.2 Token处理的Filter

      • 2.1.3 JWT属性

    • 2.2 Spring Security整合

      • 2.2.1 WebSecurityConfigurerAdapter配置

      • 2.2.2 用户从哪来

  • 3 测试

    • 4 总结

      1 简介

      Spring Security作为成熟且强大的安全框架,得到许多大厂的青睐。而作为前后端分离的SSO方案,JWT也在许多项目中应用。本文将介绍如何通过Spring Security实现JWT认证。

      用户与服务器交互大概如下:

      Springboot集成Spring Security实现JWT认证的步骤详解

      1. 客户端获取JWT,一般通过POST方法把用户名/密码传给server;

      2. 服务端接收到客户端的请求后,会检验用户名/密码是否正确,如果正确则生成JWT并返回;不正确则返回错误;

      3. 客户端拿到JWT后,在有效期内都可以通过JWT来访问资源了,一般把JWT放在请求头;一次获取,多次使用;

      4. 服务端校验JWT是否合法,合法则允许客户端正常访问,不合法则返回401。

      2 项目整合

      我们把要整合的Spring Security和JWT加入到项目的依赖中去:


      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
      </dependency>
      <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt</artifactId>
      <version>0.9.1</version>
      </dependency>

      2.1 JWT整合

      2.1.1 JWT工具类

      JWT工具类起码要具有以下功能:

      • 根据用户信息生成JWT;

      • 校验JWT是否合法,如是否被篡改、是否过期等;

      • 从JWT中解析用户信息,如用户名、权限等;

      具体代码如下:


      @Component
      public class JwtTokenProvider {

      @Autowired JwtProperties jwtProperties;

      @Autowired
      private CustomUserDetailsService userDetailsService;

      private String secretKey;

      @PostConstruct
      protected void init() {
       secretKey = Base64.getEncoder().encodeToString(jwtProperties.getSecretKey().getBytes());
      }

      public String createToken(String username, List<String> roles) {

      Claims claims = Jwts.claims().setSubject(username);
       claims.put("roles", roles);

      Date now = new Date();
       Date validity = new Date(now.getTime() + jwtProperties.getValidityInMs());

      return Jwts.builder()//
         .setClaims(claims)//
         .setIssuedAt(now)//
         .setExpiration(validity)//
         .signWith(SignatureAlgorithm.HS256, secretKey)//
         .compact();
      }

      public Authentication getAuthentication(String token) {
       UserDetails userDetails = this.userDetailsService.loadUserByUsername(getUsername(token));
       return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
      }

      public String getUsername(String token) {
       return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
      }

      public String resolveToken(HttpServletRequest req) {
       String bearerToken = req.getHeader("Authorization");
       if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
        return bearerToken.substring(7);
       }
       return null;
      }

      public boolean validateToken(String token) {
       try {
        Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);

      if (claims.getBody().getExpiration().before(new Date())) {
         return false;
        }

      return true;
       } catch (JwtException | IllegalArgumentException e) {
        throw new InvalidJwtAuthenticationException("Expired or invalid JWT token");
       }
      }

      }

      工具类还实现了另一个功能:从HTTP请求头中获取JWT。

      2.1.2 Token处理的Filter

      Filter是Security处理的关键,基本上都是通过Filter来拦截请求的。首先从请求头取出JWT,然后校验JWT是否合法,如果合法则取出Authentication保存在SecurityContextHolder里。如果不合法,则做异常处理。


      public class JwtTokenAuthenticationFilter extends GenericFilterBean {

      private JwtTokenProvider jwtTokenProvider;

      public JwtTokenAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {
       this.jwtTokenProvider = jwtTokenProvider;
      }

      @Override
      public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain)
        throws IOException, ServletException {
       HttpServletRequest request = (HttpServletRequest) req;
       HttpServletResponse response = (HttpServletResponse) res;

      try {
        String token = jwtTokenProvider.resolveToken(request);
        if (token != null && jwtTokenProvider.validateToken(token)) {
         Authentication auth = jwtTokenProvider.getAuthentication(token);

      if (auth != null) {
          SecurityContextHolder.getContext().setAuthentication(auth);
         }
        }
       } catch (InvalidJwtAuthenticationException e) {
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.getWriter().write("Invalid token");
        response.getWriter().flush();
        return;
       }

      filterChain.doFilter(req, res);
      }
      }

      对于异常处理,使用@ControllerAdvice是不行的,应该这个是Filter,在这里抛的异常还没有到DispatcherServlet,无法处理。所以Filter要自己做异常处理:


      catch (InvalidJwtAuthenticationException e) {
      response.setStatus(HttpStatus.UNAUTHORIZED.value());
      response.getWriter().write("Invalid token");
      response.getWriter().flush();
      return;
      }

      最后的return不能省略,因为已经要把输出的内容给Response了,没有必要再往后传递,否则报错


      java.lang.IllegalStateException: getWriter() has already been called

      2.1.3 JWT属性

      JWT需要配置一个密钥来加密,同时还要配置JWT令牌的有效期。


      @Configuration
      @ConfigurationProperties(prefix = "pkslow.jwt")
      public class JwtProperties {
      private String secretKey = "pkslow.key";
      private long validityInMs = 3600_000;
      //getter and setter
      }

      2.2 Spring Security整合

      Spring Security的整个框架还是比较复杂的,简化后大概如下图所示:

      Springboot集成Spring Security实现JWT认证的步骤详解

      它是通过一连串的Filter来进行安全管理。细节这里先不展开讲。

      2.2.1 WebSecurityConfigurerAdapter配置

      这个配置也可以理解为是FilterChain的配置,可以不用理解,代码很好懂它做了什么:


      @Configuration
      public class SecurityConfig extends WebSecurityConfigurerAdapter {

      @Autowired
      JwtTokenProvider jwtTokenProvider;

      @Bean
      @Override
      public AuthenticationManager authenticationManagerBean() throws Exception {
       return super.authenticationManagerBean();
      }

      @Bean
      public PasswordEncoder passwordEncoder() {
       return NoOpPasswordEncoder.getInstance();
      }

      @Override
      protected void configure(HttpSecurity http) throws Exception {
       http
        .httpBasic().disable()
        .csrf().disable()
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
        .authorizeRequests()
        .antMatchers("/auth/login").permitAll()
        .antMatchers(HttpMethod.GET, "/admin").hasRole("ADMIN")
        .antMatchers(HttpMethod.GET, "/user").hasRole("USER")
        .anyRequest().authenticated()
        .and()
        .apply(new JwtSecurityConfigurer(jwtTokenProvider));
      }
      }

      这里通过HttpSecurity配置了哪些请求需要什么权限才可以访问。

      • /auth/login用于登陆获取JWT,所以都能访问;

      • /admin只有ADMIN用户才可以访问;

      • /user只有USER用户才可以访问。

      而之前实现的Filter则在下面配置使用:


      public class JwtSecurityConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

      private JwtTokenProvider jwtTokenProvider;

      public JwtSecurityConfigurer(JwtTokenProvider jwtTokenProvider) {
       this.jwtTokenProvider = jwtTokenProvider;
      }

      @Override
      public void configure(HttpSecurity http) throws Exception {
       JwtTokenAuthenticationFilter customFilter = new JwtTokenAuthenticationFilter(jwtTokenProvider);
       http.exceptionHandling()
         .authenticationEntryPoint(new JwtAuthenticationEntryPoint())
         .and()
         .addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
      }
      }

      2.2.2 用户从哪来

      通常在Spring Security的世界里,都是通过实现UserDetailsService来获取UserDetails的。


      @Component
      public class CustomUserDetailsService implements UserDetailsService {

      private UserRepository users;

      public CustomUserDetailsService(UserRepository users) {
       this.users = users;
      }

      @Override
      public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
       return this.users.findByUsername(username)
         .orElseThrow(() -> new UsernameNotFoundException("Username: " + username + " not found"));
      }
      }

      对于UserRepository,可以从数据库中读取,或者其它用户管理中心。为了方便,我使用Map放了两个用户:


      @Repository
      public class UserRepository {

      private static final Map<String, User> allUsers = new HashMap<>();

      @Autowired
      private PasswordEncoder passwordEncoder;

      @PostConstruct
      protected void init() {
       allUsers.put("pkslow", new User("pkslow", passwordEncoder.encode("123456"), Collections.singletonList("ROLE_ADMIN")));
       allUsers.put("user", new User("user", passwordEncoder.encode("123456"), Collections.singletonList("ROLE_USER")));
      }

      public Optional<User> findByUsername(String username) {
       return Optional.ofNullable(allUsers.get(username));
      }
      }

      3 测试

      完成代码编写后,我们来测试一下:

      (1)无JWT访问,失败


      curl http://localhost:8080/admin
      {"timestamp":"2021-02-06T05:45:06.385+0000","status":403,"error":"Forbidden","message":"Access Denied","path":"/admin"}

      $ curl http://localhost:8080/user
      {"timestamp":"2021-02-06T05:45:16.438+0000","status":403,"error":"Forbidden","message":"Access Denied","path":"/user"}

      (2)admin获取JWT,密码错误则失败,密码正确则成功


      $ curl http://localhost:8080/auth/login -X POST -d '{"username":"pkslow","password":"xxxxxx"}' -H 'Content-Type: application/json'
      {"timestamp":"2021-02-06T05:47:16.254+0000","status":403,"error":"Forbidden","message":"Access Denied","path":"/auth/login"}

      $ curl http://localhost:8080/auth/login -X POST -d '{"username":"pkslow","password":"123456"}' -H 'Content-Type: application/json'
      eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJwa3Nsb3ciLCJyb2xlcyI6WyJST0xFX0FETUlOIl0sImlhdCI6MTYxMjU5MDYxNCwiZXhwIjoxNjEyNTkxMjE0fQ.d4Gi50aaOsHHqpM0d8Mh1960otnZf7rlE3x6xSfakVo

      (3)admin带JWT访问/admin,成功;访问/user失败


      $ curl http://localhost:8080/admin -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJwa3Nsb3ciLCJyb2xlcyI6WyJST0xFX0FETUlOIl0sImlhdCI6MTYxMjU5MDYxNCwiZXhwIjoxNjEyNTkxMjE0fQ.d4Gi50aaOsHHqpM0d8Mh1960otnZf7rlE3x6xSfakVo'
      you are admin

      $ curl http://localhost:8080/user -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJwa3Nsb3ciLCJyb2xlcyI6WyJST0xFX0FETUlOIl0sImlhdCI6MTYxMjU5MDYxNCwiZXhwIjoxNjEyNTkxMjE0fQ.d4Gi50aaOsHHqpM0d8Mh1960otnZf7rlE3x6xSfakVo'
      {"timestamp":"2021-02-06T05:51:23.099+0000","status":403,"error":"Forbidden","message":"Forbidden","path":"/user"}

      (4)使用过期的JWT访问,失败


      $ curl http://localhost:8080/admin -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJwa3Nsb3ciLCJyb2xlcyI6WyJST0xFX0FETUlOIl0sImlhdCI6MTYxMjU5MDQ0OSwiZXhwIjoxNjEyNTkwNTA5fQ.CSaubE4iJcYATbLmbb59aNFU1jNCwDFHUV3zIakPU64'
      Invalid token

      4 总结

      代码请查看:https://github.com/LarryDpk/pkslow-samples

      来源:https://www.pkslow.com/archives/springboot-spring-security-jwt-web

      标签:Springboot,集成,Spring,Security,jwt认证
      0
      投稿

      猜你喜欢

    • C#使用DirectX.DirectSound播放语音

      2022-08-28 01:22:33
    • C#获取鼠标在listview右键点击单元格的内容方法

      2023-10-26 12:40:47
    • 基于Elasticsearch5.4的常见问题总结

      2022-07-15 15:02:49
    • Java8中Stream的一些神操作

      2021-11-18 19:07:21
    • C++封装静态链接库和使用的详细步骤

      2021-08-28 00:54:13
    • 详解IntelliJ IDEA中TortoiseSVN修改服务器地址的方法

      2023-11-25 04:51:04
    • java程序员必须要学会的linux命令总结(推荐)

      2021-12-11 00:47:47
    • java控制台实现学生信息管理系统(集合版)

      2023-11-11 14:16:52
    • Java 遍历list和map的方法

      2023-02-06 22:23:40
    • Android实现购物车功能

      2022-08-14 19:06:55
    • SpringBoot整合SpringTask实现定时任务的流程

      2022-03-28 22:24:40
    • Spring Security结合JWT的方法教程

      2023-01-24 20:52:59
    • Spring内存缓存Caffeine的基本使用教程分享

      2023-05-26 00:30:33
    • java.lang.UnsatisfiedLinkError: %1 不是有效的Win32应用程序错误解决

      2022-06-14 23:21:51
    • Java基础之List内元素的排序性能对比

      2023-04-05 15:13:58
    • C#使用Selenium+PhantomJS抓取数据

      2022-09-13 23:18:02
    • Struts2 的国际化实现方式示例

      2022-08-10 07:26:45
    • Java中synchronized的优化措施

      2022-10-09 18:19:05
    • maven打包如何指定jdk的版本

      2022-12-21 20:59:20
    • android 将图片压缩到指定的大小的示例

      2021-07-29 23:09:23
    • asp之家 软件编程 m.aspxhome.com