我最新最全的文章都在南瓜慢说 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/pkslow-samples


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

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

Springboot集成Spring Security实现JWT认证的更多相关文章

  1. Springboot WebFlux集成Spring Security实现JWT认证

    我最新最全的文章都在南瓜慢说 www.pkslow.com,欢迎大家来喝茶! 1 简介 在之前的文章<Springboot集成Spring Security实现JWT认证>讲解了如何在传统 ...

  2. SpringBoot集成Spring Security(7)——认证流程

    文章目录 一.认证流程 二.多个请求共享认证信息 三.获取用户认证信息 在前面的六章中,介绍了 Spring Security 的基础使用,在继续深入向下的学习前,有必要理解清楚 Spring Sec ...

  3. SpringBoot集成Spring Security入门体验

    一.前言 Spring Security 和 Apache Shiro 都是安全框架,为Java应用程序提供身份认证和授权. 二者区别 Spring Security:重量级安全框架 Apache S ...

  4. SpringBoot集成Spring Security(6)——登录管理

    文章目录 一.自定义认证成功.失败处理 1.1 CustomAuthenticationSuccessHandler 1.2 CustomAuthenticationFailureHandler 1. ...

  5. SpringBoot集成Spring Security(4)——自定义表单登录

    通过前面三篇文章,你应该大致了解了 Spring Security 的流程.你应该发现了,真正的 login 请求是由 Spring Security 帮我们处理的,那么我们如何实现自定义表单登录呢, ...

  6. SpringBoot集成Spring Security(2)——自动登录

    在上一章:SpringBoot集成Spring Security(1)——入门程序中,我们实现了入门程序,本篇为该程序加上自动登录的功能. 文章目录 一.修改login.html二.两种实现方式 2. ...

  7. spring boot rest 接口集成 spring security(2) - JWT配置

    Spring Boot 集成教程 Spring Boot 介绍 Spring Boot 开发环境搭建(Eclipse) Spring Boot Hello World (restful接口)例子 sp ...

  8. SpringBoot集成Spring Security(5)——权限控制

    在第一篇中,我们说过,用户<–>角色<–>权限三层中,暂时不考虑权限,在这一篇,是时候把它完成了. 为了方便演示,这里的权限只是对角色赋予权限,也就是说同一个角色的用户,权限是 ...

  9. SpringBoot 集成Spring security

    Spring security作为一种安全框架,使用简单,能够很轻松的集成到springboot项目中,下面讲一下如何在SpringBoot中集成Spring Security.使用gradle项目管 ...

随机推荐

  1. 在C++中调用Python

    技术背景 虽然现在Python编程语言十分的火爆,但是实际上非要用一门语言去完成所有的任务,并不是说不可以,而是不合适.在一些特定的.对于性能要求比较高的场景,还是需要用到传统的C++来进行编程的.但 ...

  2. 通过Dapr实现一个简单的基于.net的微服务电商系统(十三)——istio+dapr构建多运行时服务网格之生产环境部署

    之前所有的演示都是在docker for windows上进行部署的,没有真正模拟生产环境,今天我们模拟真实环境在公有云上用linux操作如何实现istio+dapr+电商demo的部署. 目录:一. ...

  3. shell 获取MySQL查询结果并处理

    主要应用到shell for循环 定义数据库连接信息 HOST_NAME='127.0.0.1' DB_PORT='3306' DB_NAME='数据库名' USER_NAME='root' PASS ...

  4. Linux基本原则

    Bash特性 Shell shell(外壳),广义的shell可以理解为是用户的工作环境,在windows看来桌面就是一个shell,在linux看来终端就是shell 常见的shell有两种,一种是 ...

  5. 攻防世界(七)ics-06

    攻防世界系列:ics-06 1.打开题目 很LiuPi的样子,根据题目提示点开报表中心(其他的点开没什么变化) 注意到URL中有?id=1,测试其他数值无果 3.抓包对id爆破 设置1-10000,爆 ...

  6. 064.Python开发虚拟环境

    在使用 Python 开发的过程中,工程一多,难免会碰到不同的工程依赖不同版本的库的问题:亦或者是在开发过程中不想让物理环境里充斥各种各样的库,引发未来的依赖灾难.此时,我们需要对于不同的工程使用不同 ...

  7. 分布式存储ceph---ceph添加/删除osd(5)

    一.添加osd 当前ceph集群中有如下osd,现在准备新添加osd: 1.选择一个osd节点,添加好新的硬盘: 2.显示osd节点中的硬盘,并重置新的osd硬盘: 列出节点磁盘:ceph-deplo ...

  8. centos7 的Linux下ip命令替换ifconfig命令的用法

    linux的ip命令和ifconfig类似,但前者功能更强大,并旨在取代后者.使用ip命令,只需一个命令,你就能很轻松地执行一些网络管理任务.ifconfig是net-tools中已被废弃使用的一个命 ...

  9. HEX与ASCII之间装换

    static const char bcd_ascll[20]={0x30,0x31,0x32,0x33,0x34, 0x35,0x36,0x37,0x38,0x39,                 ...

  10. [leetcode] 208. 实现 Trie (前缀树)(Java)

    208. 实现 Trie (前缀树) 实现Trie树,网上教程一大堆,没啥可说的 public class Trie { private class Node { private int dumpli ...