Springboot集成Spring Security实现JWT认证
我最新最全的文章都在南瓜慢说 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/pkslow-samples
欢迎关注微信公众号<南瓜慢说>,将持续为你更新...
多读书,多分享;多写作,多整理。
Springboot集成Spring Security实现JWT认证的更多相关文章
- Springboot WebFlux集成Spring Security实现JWT认证
我最新最全的文章都在南瓜慢说 www.pkslow.com,欢迎大家来喝茶! 1 简介 在之前的文章<Springboot集成Spring Security实现JWT认证>讲解了如何在传统 ...
- SpringBoot集成Spring Security(7)——认证流程
文章目录 一.认证流程 二.多个请求共享认证信息 三.获取用户认证信息 在前面的六章中,介绍了 Spring Security 的基础使用,在继续深入向下的学习前,有必要理解清楚 Spring Sec ...
- SpringBoot集成Spring Security入门体验
一.前言 Spring Security 和 Apache Shiro 都是安全框架,为Java应用程序提供身份认证和授权. 二者区别 Spring Security:重量级安全框架 Apache S ...
- SpringBoot集成Spring Security(6)——登录管理
文章目录 一.自定义认证成功.失败处理 1.1 CustomAuthenticationSuccessHandler 1.2 CustomAuthenticationFailureHandler 1. ...
- SpringBoot集成Spring Security(4)——自定义表单登录
通过前面三篇文章,你应该大致了解了 Spring Security 的流程.你应该发现了,真正的 login 请求是由 Spring Security 帮我们处理的,那么我们如何实现自定义表单登录呢, ...
- SpringBoot集成Spring Security(2)——自动登录
在上一章:SpringBoot集成Spring Security(1)——入门程序中,我们实现了入门程序,本篇为该程序加上自动登录的功能. 文章目录 一.修改login.html二.两种实现方式 2. ...
- spring boot rest 接口集成 spring security(2) - JWT配置
Spring Boot 集成教程 Spring Boot 介绍 Spring Boot 开发环境搭建(Eclipse) Spring Boot Hello World (restful接口)例子 sp ...
- SpringBoot集成Spring Security(5)——权限控制
在第一篇中,我们说过,用户<–>角色<–>权限三层中,暂时不考虑权限,在这一篇,是时候把它完成了. 为了方便演示,这里的权限只是对角色赋予权限,也就是说同一个角色的用户,权限是 ...
- SpringBoot 集成Spring security
Spring security作为一种安全框架,使用简单,能够很轻松的集成到springboot项目中,下面讲一下如何在SpringBoot中集成Spring Security.使用gradle项目管 ...
随机推荐
- Windows进程间通讯(IPC)----内存映射文件
内存映射文件原理 内存映射文件是通过在虚拟地址空间中预留一块区域,然后通过从磁盘中已存在的文件为其调度物理存储器,访问此虚拟内存空间就相当于访问此磁盘文件了. 内存映射文件实现过程 HANDLE hF ...
- SparkSQL电商用户画像(五)之用户画像开发(客户基本属性表)
7.电商用户画像开发 7.1用户画像--数据开发的步骤 u 数据开发前置依赖 -需求确定 pv uv topn -建模确定表结构 create table t1(pv int,uv int,topn ...
- 解读vue-server-renderer源码并在react中的实现
前言 在博客开发的过程中,有这样一个需求想解决,就是在SSR开发环境中,服务端的代码是是直接通过webpack打包成文件(因为里面包含同构的代码,就是服务端与客户端共享前端的组件代码),写到磁盘里 ...
- 在Visual Studio 中使用git——浏览版本库(七)
在Visual Studio 中使用git--什么是Git(一) 在Visual Studio 中使用git--给Visual Studio安装 git插件(二) 在Visual Studio 中使用 ...
- [前端] AJAX
背景 Asynchronous JavaScript And XML:异步js和XML,可实现异步刷新 用途 验证提交的用户名是否已存在 不使用AJAX,需要提交数据后,刷新页面来验证 使用AJAX, ...
- Linux_配置主DNS服务(基础)
[RHEL8]-DNSserver:[Centos7.4]-DNSclient !!!测试环境我们首关闭防火墙和selinux(DNSserver和DNSclient都需要) [root@localh ...
- 043.Python线程基本介绍
一 线程的基本概念 1.1 进程和线程 进程是资源分配的最小单位 线程是计算机中调度的最小单位 进程池: 开启过多的进程并不一走提高你的效率, 如果cp负载任务过多,平均单个任务执行的效率就会低,反而 ...
- 关于步进电机驱动板,tb6560
参考的,淘宝上买来的步进电机S曲线驱动方法,发现 他程序输出的PWM波形全是方波, 占空比为50% 而且他 修改这两个数来输出波形,所以 我打算参考这个来写一个驱动 TIMX_CNT中放置的是当前计 ...
- spark-steaming的exactly-once
spark实时计算中会存在数据丢失和数据重复计算的场景, 在receiver收到数据且通过driver的调度executor开始计算数据的时候如果driver突然崩溃,则此时executor就会被杀掉 ...
- pyqt安装
一.安装PyQt5 pip install PyQt5 二.安装PyQt-tools pip install PyQt-tools *注:mac不需要安装PyQt-tools,能够正常使用,只支持Wi ...