我最新最全的文章都在 南瓜慢说 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中解析用户信息,如用户名、权限等;

具体代码如下:

@Componentpublic 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的配置,能够不必了解,代码很好懂它做了什么:

@Configurationpublic 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的。

@Componentpublic 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放了两个用户:

@Repositorypublic 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...


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

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