> 原文阅读:https://blog.csdn.net/jpgzhu/article/details/105200598
> - 本文内容已应用于dmp框架,并对com.keyou.dmp.util.JwtTokenUtils#getAuthentication进行了优化,增加了权限信息的存放,最新代码请查看dmp;
> - 本框架是在SpringSecurity的基础上引入了jwt,并没有引入oauth2。当然SpringSecurity在引入OAuth2的前提下,可以使用jwt作为token存储方案,如ocp就是这样使用的。
#### 前言
> 微服务架构,前后端分离目前已成为互联网项目开发的业界标准,其核心思想就是前端(APP、小程序、H5页面等)通过调用后端的API接口,提交及返回JSON数据进行交互。
> 在前后端分离项目中,首先要解决的就是登录及授权的问题。微服务架构下,传统的session认证限制了应用的扩展能力,无状态的JWT认证方法应运而生,该认证机制特别适用于分布式站点的单点登录(SSO)场景
#### 目录
> 该文会通过创建SpringBoot项目整合SpringSecurity,实现完整的JWT认证机制,主要步骤如下:
>
> 1. 创建SpringBoot工程
> 2. 导入SpringSecurity与JWT的相关依赖
> 3. 定义SpringSecurity需要的基础处理类
> 4. 构建JWT token工具类
> 5. 实现token验证的过滤器
> 6. SpringSecurity的关键配置
> 7. 编写Controller进行测试
#### 1、创建SpringBoot工程
![SpringBoot工程项目目录](https://img-blog.csdnimg.cn/20200330155505528.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2pwZ3podQ==,size_16,color_FFFFFF,t_70)
#### 2、导入SpringSecurity与JWT的相关依赖
pom文件加入以下依赖
```java
<!--Security框架-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
...
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.10.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.10.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.10.6</version>
</dependency>
```
#### 3.定义SpringSecurity需要的基础处理类
application.yml配置中加入jwt配置信息:
```java
#jwt
jwt:
header: Authorization
# 令牌前缀
token-start-with: Bearer
# 使用Base64对该令牌进行编码
base64-secret: XXXXXXXXXXXXXXXX(制定您的密钥)
# 令牌过期时间 此处单位/毫秒
token-validity-in-seconds: 14400000
```
创建一个jwt的配置类,并注入Spring,便于程序中灵活调用
```java
@Data
@Configuration
@ConfigurationProperties(prefix = "jwt")
public class JwtSecurityProperties {
/** Request Headers : Authorization */
private String header;
/** 令牌前缀,最后留个空格 Bearer */
private String tokenStartWith;
/** Base64对该令牌进行编码 */
private String base64Secret;
/** 令牌过期时间 此处单位/毫秒 */
private Long tokenValidityInSeconds;
/**返回令牌前缀 */
public String getTokenStartWith() {
return tokenStartWith + " ";
}
}
```
定义无权限访问类
```java
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
response.sendError(HttpServletResponse.SC_FORBIDDEN, accessDeniedException.getMessage());
}
}
```
定义认证失败处理类
```java
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException==null?"Unauthorized":authException.getMessage());
}
}
```
#### 4. 构建JWT token工具类
工具类实现创建token与校验token功能
```java
@Slf4j
@Component
public class JwtTokenUtils implements InitializingBean {
private final JwtSecurityProperties jwtSecurityProperties;
private static final String AUTHORITIES_KEY = "auth";
private Key key;
public JwtTokenUtils(JwtSecurityProperties jwtSecurityProperties) {
this.jwtSecurityProperties = jwtSecurityProperties;
}
@Override
public void afterPropertiesSet() {
byte[] keyBytes = Decoders.BASE64.decode(jwtSecurityProperties.getBase64Secret());
this.key = Keys.hmacShaKeyFor(keyBytes);
}
public String createToken (Map<String, Object> claims) {
return Jwts.builder()
.claim(AUTHORITIES_KEY, claims)
.setId(UUID.randomUUID().toString())
.setIssuedAt(new Date())
.setExpiration(new Date((new Date()).getTime() + jwtSecurityProperties.getTokenValidityInSeconds()))
.compressWith(CompressionCodecs.DEFLATE)
.signWith(key,SignatureAlgorithm.HS512)
.compact();
}
public Date getExpirationDateFromToken(String token) {
Date expiration;
try {
final Claims claims = getClaimsFromToken(token);
expiration = claims.getExpiration();
} catch (Exception e) {
expiration = null;
}
return expiration;
}
public Authentication getAuthentication(String token) {
Claims claims = Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(token)
.getBody();
Collection<? extends GrantedAuthority> authorities =
Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
HashMap map =(HashMap) claims.get("auth");
User principal = new User(map.get("user").toString(), map.get("password").toString(), authorities);
return new UsernamePasswordAuthenticationToken(principal, token, authorities);
}
public boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(key).parseClaimsJws(authToken);
return true;
} catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
log.info("Invalid JWT signature.");
e.printStackTrace();
} catch (ExpiredJwtException e) {
log.info("Expired JWT token.");
e.printStackTrace();
} catch (UnsupportedJwtException e) {
log.info("Unsupported JWT token.");
e.printStackTrace();
} catch (IllegalArgumentException e) {
log.info("JWT token compact of handler are invalid.");
e.printStackTrace();
}
return false;
}
private Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
}
```
#### 5.实现token验证的过滤器
该类继承OncePerRequestFilter,顾名思义,它能够确保在一次请求中只通过一次filter
该类使用JwtTokenUtils工具类进行token校验
```java
@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private JwtTokenUtils jwtTokenUtils;
public JwtAuthenticationTokenFilter(JwtTokenUtils jwtTokenUtils) {
this.jwtTokenUtils = jwtTokenUtils;
}
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
JwtSecurityProperties jwtSecurityProperties = SpringContextHolder.getBean(JwtSecurityProperties.class);
String requestRri = httpServletRequest.getRequestURI();
//获取request token
String token = null;
String bearerToken = httpServletRequest.getHeader(jwtSecurityProperties.getHeader());
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(jwtSecurityProperties.getTokenStartWith())) {
token = bearerToken.substring(jwtSecurityProperties.getTokenStartWith().length());
}
if (StringUtils.hasText(token) && jwtTokenUtils.validateToken(token)) {
Authentication authentication = jwtTokenUtils.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
log.debug("set Authentication to security context for '{}', uri: {}", authentication.getName(), requestRri);
} else {
log.debug("no valid JWT token found, uri: {}", requestRri);
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
```
> 根据SpringBoot官方让重复执行的filter实现一次执行过程的解决方案,参见官网地址:https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-disable-registration-of-a-servlet-or-filter
> 在SpringBoot启动类中,加入以下代码:
```java
@Bean
public FilterRegistrationBean registration(JwtAuthenticationTokenFilter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean(filter);
registration.setEnabled(false);
return registration;
}
123456
```
#### 6. SpringSecurity的关键配置
SpringBoot推荐使用配置类来代替xml配置,该类中涉及了以上几个bean来供security使用
- JwtAccessDeniedHandler :无权限访问
- jwtAuthenticationEntryPoint :认证失败处理
- jwtAuthenticationTokenFilter :token验证的过滤器
```java
package com.zhuhuix.startup.security.config;
import com.fasterxml.jackson.core.filter.TokenFilter;
import com.zhuhuix.startup.security.security.JwtAccessDeniedHandler;
import com.zhuhuix.startup.security.security.JwtAuthenticationEntryPoint;
import com.zhuhuix.startup.security.security.JwtAuthenticationTokenFilter;
import com.zhuhuix.startup.security.utils.JwtTokenUtils;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* Spring Security配置类
*
* @author zhuhuix
* @date 2020-03-25
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtTokenUtils jwtTokenUtils;
public WebSecurityConfig(JwtAccessDeniedHandler jwtAccessDeniedHandler, JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint, JwtTokenUtils jwtTokenUtils) {
this.jwtAccessDeniedHandler = jwtAccessDeniedHandler;
this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
this.jwtTokenUtils = jwtTokenUtils;
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// 禁用 CSRF
.csrf().disable()
// 授权异常
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
// 防止iframe 造成跨域
.and()
.headers()
.frameOptions()
.disable()
// 不创建会话
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 放行静态资源
.antMatchers(
HttpMethod.GET,
"/*.html",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/webSocket/**"
).permitAll()
// 放行swagger
.antMatchers("/swagger-ui.html").permitAll()
.antMatchers("/swagger-resources/**").permitAll()
.antMatchers("/webjars/**").permitAll()
.antMatchers("/*/api-docs").permitAll()
// 放行文件访问
.antMatchers("/file/**").permitAll()
// 放行druid
.antMatchers("/druid/**").permitAll()
// 放行OPTIONS请求
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
//允许匿名及登录用户访问
.antMatchers("/api/auth/**", "/error/**").permitAll()
// 所有请求都需要认证
.anyRequest().authenticated();
// 禁用缓存
httpSecurity.headers().cacheControl();
// 添加JWT filter
httpSecurity
.apply(new TokenConfigurer(jwtTokenUtils));
}
public class TokenConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private final JwtTokenUtils jwtTokenUtils;
public TokenConfigurer(JwtTokenUtils jwtTokenUtils){
this.jwtTokenUtils = jwtTokenUtils;
}
@Override
public void configure(HttpSecurity http) {
JwtAuthenticationTokenFilter customFilter = new JwtAuthenticationTokenFilter(jwtTokenUtils);
http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
}
}
}
```
#### 7. 编写Controller进行测试
登录逻辑:传递user与password参数,返回token
```java
@Slf4j
@RestController
@RequestMapping("/api/auth")
@Api(tags = "系统授权接口")
public class AuthController {
private final JwtTokenUtils jwtTokenUtils;
public AuthController(JwtTokenUtils jwtTokenUtils) {
this.jwtTokenUtils = jwtTokenUtils;
}
@ApiOperation("登录授权")
@GetMapping(value = "/login")
public String login(String user,String password){
Map map = new HashMap();
map.put("user",user);
map.put("password",password);
return jwtTokenUtils.createToken(map);
}
}
```
使用IDEA Rest Client测试如下:![Request请求](https://img-blog.csdnimg.cn/20200330190123833.png)![Response返回token](https://img-blog.csdnimg.cn/20200330190153673.png)
验证逻辑:传递token,验证成功后返回用户信息
![Request请求,Headers加入Authorization:(token值)](https://img-blog.csdnimg.cn/20200330193526960.png)![Response结果](https://img-blog.csdnimg.cn/20200330193639420.png)
token验证错误返回401:
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200330201138809.png)
>其他资料:
[SpringBoot整合SpringSecurity实现JWT认证](https://blog.csdn.net/jpgzhu/article/details/105200598)
[Spring Security登录认证【前后端分离】](https://blog.csdn.net/godleaf/article/details/108318403)
[Springboot+SpringSecurity_jwt整合原理](https://www.processon.com/view/5eee114a07912929cb51e062?fromnew=1#map)
- 简介
- 更新说明
- 其他作品
- 第一部分 Java框架基础
- 第一章 Java基础
- 多线程实战
- 尝试一下Guava带返回值的多线程处理类ListenableFuture
- LocalDate和Date有什么区别
- JAVA8接口增强实践
- 第二章 Spring框架基础
- MVC究竟是个啥?
- @ApiImplicitParam
- 七种方式,教你在SpringBoot初始化时搞点事情!
- Spring事务状态
- maven
- Mybatis小总结
- mybatis-plus的使用
- 第三章 SpringSecurity实战
- 基于SpringSecurity+jwt的用户认证
- spring-security-oauth2
- 第四章 数据库
- mysql
- mysql授权
- mysql数据库三个关键性能指标--TPS\QPS\IOPS
- 梳理一下那些年Mysql的弱语法可能会踩的坑
- 关于Mysql的“字符串”数值的转换和使用
- 凭这一文咱把事务讲透
- Mysql性能优化
- 查询性能优化
- 不常用的一些语法
- elasticsearch
- elasticsearch文档操作
- 索引的基本操作
- java操作ElaticSearch
- elasticsearch中的各种查询
- DB与ES混合应用可能存在的问题及解决方案探索
- 使用es必须要知道的一些知识点:索引篇
- Es中的日期操作
- MongoDB
- 入门篇(了解非关系型数据库 NoSQL - MongoDB)
- 集群分片 (高级篇)
- 互联网大厂的建表规范
- 第五章 中间件
- nginx
- nginx动静分离配置,这个雷你踩过吗?
- Canal
- Sharding-jdbc
- 水平分库实践
- kafka
- 第六章 版本管理
- git
- Not currently on any branch 情况提交版本
- 第七章 IO编程
- 第八章 JVM实战调优
- jvisualvm
- jstat
- 第二部分 高级项目实战篇
- 第一章 微信开发实战
- 第二章 文件处理
- 使用EasyExcel处理导入导出
- 第三章 踩坑指南
- 邮件发送功能
- 第三部分 架构实战篇
- 第一章 架构实战原则
- 接口防止重复调用的一种方案
- 第二章 高并发缓存一致性管理办法
- 第三章 异地多活场景下的数据同步之道
- 第四章 用户体系
- 集成登录
- auth-sso的管理
- 第五章 分库分表场景
- 第六章 秒杀与高并发
- 秒杀场景
- 第七章 业务中台
- 中台的使用效果是怎样的?
- 通用黑白名单方案
- 第八章 领域驱动设计
- 第十一章 微服务实战
- Nacos多环境管理之道
- logback日志双写问题及Springboot项目正确的启动方式
- 第四部分 优雅的代码
- java中的链式编程
- 面向对象
- 开发原则
- Stream操作案例分享
- 注重性能的代码
- 第五部分 谈谈成长
- 新手入门指北
- 不可不知的调试技巧
- 构建自己的知识体系
- 我是如何做笔记的
- 有效的提问
- 谨防思维定势
- 学会与上级沟通
- 想清楚再去做
- 碎片化学习
- 第六部分 思维导图(付费)
- 技术基础篇
- 技术框架篇
- 数据存储篇
- 项目实战篇
- 第七部分 吾爱开源
- 7-1 麻雀聊天
- 项目启动
- 前端登录无请求问题解决
- websocket测试
- 7-2 ocp微服务框架
- evm框架集成
- 项目构建与集成
- zentao-center
- 二次开发:初始框架的搭建
- 二次开发:增加细分菜单、权限到应用
- 7-3 书栈网
- 项目启动
- 源码分析
- 我的书架
- 文章发布机制
- IM
- 第八章 团队管理篇
- 大厂是怎么运作的
- 第九章 码山有道
- 简历内推
- 联系我内推
- 第十章 学点前端
- Vue