我最新最全的文章都在 南瓜慢说 www.pkslow.com ,欢送大家来喝茶!
1 简介
Spring Security
作为成熟且弱小的平安框架,失去许多大厂的青眼。而作为前后端拆散的SSO
计划,JWT
也在许多我的项目中利用。本文将介绍如何通过Spring Security
实现JWT
认证。
用户与服务器交互大略如下:
- 客户端获取
JWT
,个别通过POST
办法把用户名/明码传给server
; - 服务端接管到客户端的申请后,会测验用户名/明码是否正确,如果正确则生成
JWT
并返回;不正确则返回谬误; - 客户端拿到
JWT
后,在有效期
内都能够通过JWT
来拜访资源了,个别把JWT
放在申请头;一次获取,屡次应用; - 服务端校验
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中解析用户信息,如用户名、权限等;
具体代码如下:
@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
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
的整个框架还是比较复杂的,简化后大略如下图所示:
它是通过一连串的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...
欢送关注微信公众号<南瓜慢说>,将继续为你更新...
多读书,多分享;多写作,多整顿。