乐趣区

关于java:Springboot集成Spring-Security实现JWT认证

我最新最全的文章都在 南瓜慢说 www.pkslow.com,欢送大家来喝茶!

1 简介

Spring Security作为成熟且弱小的平安框架,失去许多大厂的青眼。而作为前后端拆散的 SSO 计划,JWT也在许多我的项目中利用。本文将介绍如何通过 Spring Security 实现 JWT 认证。

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

  1. 客户端获取 JWT,个别通过POST 办法把用户名 / 明码传给server
  2. 服务端接管到客户端的申请后,会测验用户名 / 明码是否正确,如果正确则生成 JWT 并返回;不正确则返回谬误;
  3. 客户端拿到 JWT 后,在 有效期 内都能够通过 JWT 来拜访资源了,个别把 JWT 放在申请头;一次获取,屡次应用;
  4. 服务端校验 JWT 是否非法,非法则容许客户端失常拜访,不非法则返回 401。

2 我的项目整合

咱们把要整合的 Spring SecurityJWT退出到我的项目的依赖中去:

<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

FilterSecurity 解决的要害,基本上都是通过 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的整个框架还是比较复杂的,简化后大略如下图所示:

它是通过一连串的 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

对于用户 user 同样能够测试,这里不列出来了。

4 总结

代码请查看:https://github.com/LarryDpk/p…


欢送关注微信公众号 <南瓜慢说>,将继续为你更新 …

多读书,多分享;多写作,多整顿。

退出移动版