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

网友投稿 282 2022-11-03


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

1 简介

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

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

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

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

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

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

2 项目整合

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

org.springframework.boot

spring-boot-starter-web

org.springframework.boot

spring-boot-starter-security

io.jsonwebtoken

jjwt

0.9.1

2.1 JWT整合

2.1.1 JWT工具类

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

根据用户信息生成JWT;

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

http:// 从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 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 = 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 {

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 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 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

以上就是Springboot集成Spring Security实现JWT认证的步骤详解的详细内容,更多关于Springboot集成Spring Security的资料请关注我们其它相关文章!


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:dhl国际包裹跟踪查询API(dhl国际包裹跟踪查询中文)
下一篇:解放运维的双手,谈自动化运维管理平台设计
相关文章

 发表评论

暂时没有评论,来抢沙发吧~