我最新最全的文章都在 南瓜慢说 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 中解析用户信息,如用户名、权限等;
具体代码如下:
@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
的整个框架还是比较复杂的,简化后大略如下图所示:
它是通过一连串的 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…
欢送关注微信公众号 <南瓜慢说>,将继续为你更新 …
多读书,多分享;多写作,多整顿。