Spring Boot 使用 JWT 进行身份和权限验证
上周写了一个 适合初学者入门 Spring Security With JWT 的 Demo,这篇文章主要是对代码中涉及到的比较重要的知识点的说明。
适合初学者入门 Spring Security With JWT 的 Demo 这篇文章中说到了要在十一假期期间对代码进行讲解说明,但是,你们懂得,到了十一就一拖再拖,眼看着今天就是十一的尾声了,抽了一下午完成了这部分内容。
明天就要上班了,擦擦眼泪,还是一条好汉!扶我起来,学不动了。
如果 对 JWT 不了解的话,可以看前几天发的这篇原创文章:《一问带你区分清楚Authentication,Authorization以及Cookie、Session、Token》。
Demo 地址:https://github.com/Snailclimb/spring-security-jwt-guide 。
Controller
这个是 UserControler
主要用来验证权限配置是否生效。
getAllUser()
方法被注解@PreAuthorize("hasAnyRole('ROLE_DEV','ROLE_PM')")
修饰代表这个方法可以被 DEV,PM 这两个角色访问,而deleteUserById()
被注解@PreAuthorize("hasAnyRole('ROLE_ADMIN')")
修饰代表只能被 ADMIN 访问。
/**
* @author shuang.kou
*/
@RestController
@RequestMapping("/api")
public class UserController {
private final UserService userService;
private final CurrentUser currentUser;
public UserController(UserService userService, CurrentUser currentUser) {
this.userService = userService;
this.currentUser = currentUser;
}
@GetMapping("/users")
@PreAuthorize("hasAnyRole('ROLE_DEV','ROLE_PM')")
public ResponseEntity<Page<User>> getAllUser(@RequestParam(value = "pageNum", defaultValue = "0") int pageNum, @RequestParam(value = "pageSize", defaultValue = "10") int pageSize) {
System.out.println("当前访问该接口的用户为:" + currentUser.getCurrentUser().toString());
Page<User> allUser = userService.getAllUser(pageNum, pageSize);
return ResponseEntity.ok().body(allUser);
}
@DeleteMapping("/user")
@PreAuthorize("hasAnyRole('ROLE_ADMIN')")
public ResponseEntity<User> deleteUserById(@RequestParam("username") String username) {
userService.deleteUserByUserName(username);
return ResponseEntity.ok().build();
}
}
安全认证工具类
里面主要有一些常用的方法比如 生成 token 以及解析 token 获取相关信息等等方法。
/**
* @author shuang.kou
*/
public class JwtTokenUtils {
/**
* 生成足够的安全随机密钥,以适合符合规范的签名
*/
private static byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(SecurityConstants.JWT_SECRET_KEY);
private static SecretKey secretKey = Keys.hmacShaKeyFor(apiKeySecretBytes);
public static String createToken(String username, List<String> roles, boolean isRememberMe) {
long expiration = isRememberMe ? SecurityConstants.EXPIRATION_REMEMBER : SecurityConstants.EXPIRATION;
String tokenPrefix = Jwts.builder()
.setHeaderParam("typ", SecurityConstants.TOKEN_TYPE)
.signWith(secretKey, SignatureAlgorithm.HS256)
.claim(SecurityConstants.ROLE_CLAIMS, String.join(",", roles))
.setIssuer("SnailClimb")
.setIssuedAt(new Date())
.setSubject(username)
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
.compact();
return SecurityConstants.TOKEN_PREFIX + tokenPrefix;
}
private boolean isTokenExpired(String token) {
Date expiredDate = getTokenBody(token).getExpiration();
return expiredDate.before(new Date());
}
public static String getUsernameByToken(String token) {
return getTokenBody(token).getSubject();
}
/**
* 获取用户所有角色
*/
public static List<SimpleGrantedAuthority> getUserRolesByToken(String token) {
String role = (String) getTokenBody(token)
.get(SecurityConstants.ROLE_CLAIMS);
return Arrays.stream(role.split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
private static Claims getTokenBody(String token) {
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody();
}
}
过滤器
先来看一下比较重要的两个过滤器。
第一个过滤器主要用于根据用户的用户名和密码进行登录验证(用户请求中必须有用户名和密码这两个参数),它继承了 UsernamePasswordAuthenticationFilter
并且重写了下面三个方法:
attemptAuthentication()
: 验证用户身份。successfulAuthentication()
: 用户身份验证成功后调用的方法。unsuccessfulAuthentication()
: 用户身份验证失败后调用的方法。
/**
* @author shuang.kou
* 如果用户名和密码正确,那么过滤器将创建一个JWT Token 并在HTTP Response 的header中返回它,格式:token: "Bearer +具体token值"
*/
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private ThreadLocal<Boolean> rememberMe = new ThreadLocal<>();
private AuthenticationManager authenticationManager;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
// 设置登录请求的 URL
super.setFilterProcessesUrl(SecurityConstants.AUTH_LOGIN_URL);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
ObjectMapper objectMapper = new ObjectMapper();
try {
// 从输入流中获取到登录的信息
LoginUser loginUser = objectMapper.readValue(request.getInputStream(), LoginUser.class);
rememberMe.set(loginUser.getRememberMe());
// 这部分和attemptAuthentication方法中的源码是一样的,
// 只不过由于这个方法源码的是把用户名和密码这些参数的名字是死的,所以我们重写了一下
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
loginUser.getUsername(), loginUser.getPassword());
return authenticationManager.authenticate(authRequest);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
/**
* 如果验证成功,就生成token并返回
*/
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authentication) {
JwtUser jwtUser = (JwtUser) authentication.getPrincipal();
List<String> roles = jwtUser.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
// 创建 Token
String token = JwtTokenUtils.createToken(jwtUser.getUsername(), roles, rememberMe.get());
// Http Response Header 中返回 Token
response.setHeader(SecurityConstants.TOKEN_HEADER, token);
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException authenticationException) throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authenticationException.getMessage());
}
}
这个过滤器继承了 BasicAuthenticationFilter
,主要用于处理身份认证后才能访问的资源,它会检查 HTTP 请求是否存在带有正确令牌的 Authorization 标头并验证 token 的有效性。
/**
* 过滤器处理所有HTTP请求,并检查是否存在带有正确令牌的Authorization标头。例如,如果令牌未过期或签名密钥正确。
*
* @author shuang.kou
*/
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
private static final Logger logger = Logger.getLogger(JWTAuthorizationFilter.class.getName());
public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
String authorization = request.getHeader(SecurityConstants.TOKEN_HEADER);
// 如果请求头中没有Authorization信息则直接放行了
if (authorization == null || !authorization.startsWith(SecurityConstants.TOKEN_PREFIX)) {
chain.doFilter(request, response);
return;
}
// 如果请求头中有token,则进行解析,并且设置授权信息
SecurityContextHolder.getContext().setAuthentication(getAuthentication(authorization));
super.doFilterInternal(request, response, chain);
}
/**
* 这里从token中获取用户信息并新建一个token
*/
private UsernamePasswordAuthenticationToken getAuthentication(String authorization) {
String token = authorization.replace(SecurityConstants.TOKEN_PREFIX, "");
try {
String username = JwtTokenUtils.getUsernameByToken(token);
// 通过 token 获取用户具有的角色
List<SimpleGrantedAuthority> userRolesByToken = JwtTokenUtils.getUserRolesByToken(token);
if (!StringUtils.isEmpty(username)) {
return new UsernamePasswordAuthenticationToken(username, null, userRolesByToken);
}
} catch (SignatureException | ExpiredJwtException exception) {
logger.warning("Request to parse JWT with invalid signature . Detail : " + exception.getMessage());
}
return null;
}
}
当用户使用 token 对需要权限才能访问的资源进行访问的时候,这个类是主要用到的,下面按照步骤来说一说每一步到底都做了什么。
- 当用户使用系统返回的 token 信息进行登录的时候 ,会首先经过
doFilterInternal()
方法,这个方法会从请求的 Header 中取出 token 信息,然后判断 token 信息是否为空以及 token 信息格式是否正确。 - 如果请求头中有 token 并且 token 的格式正确,则进行解析并判断 token 的有效性,然后会在 Spring Security 全局设置授权信息
SecurityContextHolder.getContext().setAuthentication(getAuthentication(authorization));
CurrentUser
我们在讲过滤器的时候说过,当认证成功的用户访问系统的时候,它的认证信息会被设置在 Spring Security 全局中。那么,既然这样,我们在其他地方获取到当前登录用户的授权信息也就很简单了,通过SecurityContextHolder.getContext().getAuthentication();
方法即可。为此,我们实现了一个专门用来获取当前用户的类:
/**
* @author shuang.kou
* 获取当前请求的用户
*/
@Component
public class CurrentUser {
private final UserDetailsServiceImpl userDetailsService;
public CurrentUser(UserDetailsServiceImpl userDetailsService) {
this.userDetailsService = userDetailsService;
}
public JwtUser getCurrentUser() {
return (JwtUser) userDetailsService.loadUserByUsername(getCurrentUserName());
}
/**
* TODO:由于在JWTAuthorizationFilter这个类注入UserDetailsServiceImpl一致失败,
* 导致无法正确查找到用户,所以存入Authentication的Principal为从 token 中取出的当前用户的姓名
*/
private static String getCurrentUserName() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getPrincipal() != null) {
return (String) authentication.getPrincipal();
}
return null;
}
}
异常相关
JWTAccessDeniedHandler
实现了AccessDeniedHandler
主要用来解决认证过的用户访问需要权限才能访问的资源时的异常。
/**
* @author shuang.kou
* AccessDeineHandler 用来解决认证过的用户访问需要权限才能访问的资源时的异常
*/
public class JWTAccessDeniedHandler implements AccessDeniedHandler {
/**
* 当用户尝试访问需要权限才能的REST资源而权限不足的时候,
* 将调用此方法发送401响应以及错误信息
*/
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
accessDeniedException = new AccessDeniedException("Sorry you don not enough permissions to access it!");
response.sendError(HttpServletResponse.SC_FORBIDDEN, accessDeniedException.getMessage());
}
}
JWTAuthenticationEntryPoint
实现了 AuthenticationEntryPoint
用来解决匿名用户访问需要权限才能访问的资源时的异常
/**
* @author shuang.kou
* AuthenticationEntryPoint 用来解决匿名用户访问需要权限才能访问的资源时的异常
*/
public class JWTAuthenticationEntryPoint implements AuthenticationEntryPoint {
/**
* 当用户尝试访问需要权限才能的REST资源而不提供Token或者Token过期时,
* 将调用此方法发送401响应以及错误信息
*/
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
}
}
配置类
在 SecurityConfig 配置类中我们主要配置了:
- 密码编码器
BCryptPasswordEncoder
(存入数据库的密码需要被加密)。 - 为
AuthenticationManager
设置自定义的UserDetailsService
以及密码编码器; - 在 Spring Security 配置指定了哪些路径下的资源需要验证了的用户才能访问、哪些不需要以及哪些资源只能被特定角色访问;
- 将我们自定义的两个过滤器添加到 Spring Security 配置中;
- 将两个自定义处理权限认证方面的异常类添加到 Spring Security 配置中;
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsServiceImpl userDetailsServiceImpl;
/**
* 密码编码器
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService createUserDetailsService() {
return userDetailsServiceImpl;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 设置自定义的userDetailsService以及密码编码器
auth.userDetailsService(userDetailsServiceImpl).passwordEncoder(bCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and()
// 禁用 CSRF
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/auth/login").permitAll()
// 指定路径下的资源需要验证了的用户才能访问
.antMatchers("/api/**").authenticated()
.antMatchers(HttpMethod.DELETE, "/api/**").hasRole("ADMIN")
// 其他都放行了
.anyRequest().permitAll()
.and()
//添加自定义Filter
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
// 不需要session(不创建会话)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 授权异常处理
.exceptionHandling().authenticationEntryPoint(new JWTAuthenticationEntryPoint())
.accessDeniedHandler(new JWTAccessDeniedHandler());
}
}
跨域:
在这里踩的一个坑是:如果你没有设置exposedHeaders("Authorization")
暴露 header 中的"Authorization"属性给客户端应用程序的话,前端是获取不到 token 信息的。
@Configuration
public class CorsConfiguration implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
//暴露header中的其他属性给客户端应用程序
//如果不设置这个属性前端无法通过response header获取到Authorization也就是token
.exposedHeaders("Authorization")
.allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT")
.maxAge(3600);
}
}
JWT 优缺点分析
优点
- 无状态,服务器不需要存储 Session 信息。
- 有效避免了CSRF 攻击。
- 适合移动端应用。
- 单点登录友好。
缺点
- 注销登录等场景下 token 还有效
- token 的续签问题
公众号
如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。
Spring Boot 使用 JWT 进行身份和权限验证的更多相关文章
- spring boot:spring security整合jwt实现登录和权限验证(spring boot 2.3.3)
一,为什么使用jwt? 1,什么是jwt? Json Web Token, 它是JSON风格的轻量级的授权和身份认证规范, 可以实现无状态.分布式的Web应用授权 2,jwt的官网: https:// ...
- Spring Boot初识(4)- Spring Boot整合JWT
一.本文介绍 上篇文章讲到Spring Boot整合Swagger的时候其实我就在思考关于接口安全的问题了,在这篇文章了我整合了JWT用来保证接口的安全性.我会先简单介绍一下JWT然后在上篇文章的基础 ...
- Spring Boot(十四):spring boot整合shiro-登录认证和权限管理
Spring Boot(十四):spring boot整合shiro-登录认证和权限管理 使用Spring Boot集成Apache Shiro.安全应该是互联网公司的一道生命线,几乎任何的公司都会涉 ...
- Spring Boot Security JWT 整合实现前后端分离认证示例
前面两章节我们介绍了 Spring Boot Security 快速入门 和 Spring Boot JWT 快速入门,本章节使用 JWT 和 Spring Boot Security 构件一个前后端 ...
- 基于Spring Cloud、JWT 的微服务权限系统设计
基于Spring Cloud.JWT 的微服务权限系统设计 https://gitee.com/log4j/pig https://github.com/kioyong/spring-cloud-de ...
- 部署spring boot + Vue遇到的坑(权限、刷新404、跨域、内存)
部署spring boot + Vue遇到的坑(权限.刷新404.跨域.内存) 项目背景是采用前后端分离,前端使用vue,后端使用springboot. 工具 工欲善其事必先利其器,我们先找一个操作L ...
- spring boot Shiro JWT整合
一个api要支持H5, PC和APP三个前端,如果使用session的话对app不是很友好,而且session有跨域攻击的问题,所以选择了JWT 1.导入依赖包 <dependency> ...
- (转)Spring Boot (十四): Spring Boot 整合 Shiro-登录认证和权限管理
http://www.ityouknow.com/springboot/2017/06/26/spring-boot-shiro.html 这篇文章我们来学习如何使用 Spring Boot 集成 A ...
- Spring Boot (十四): Spring Boot 整合 Shiro-登录认证和权限管理
这篇文章我们来学习如何使用 Spring Boot 集成 Apache Shiro .安全应该是互联网公司的一道生命线,几乎任何的公司都会涉及到这方面的需求.在 Java 领域一般有 Spring S ...
随机推荐
- Linux---vim编辑文本文件
1.vim工作模式 普通模式:该模式下可以快速移动光标位置,能够执行对文本的快捷编辑,但是不能够在文本中输入内容: 插入模式:该模式主要用于在文本中插入内容,是文本输入时最常使用的模式: 命令模式:该 ...
- 使用map将字数组里的对象重新组装
变为数组 ["扬子","北京","上海海吉雅"] // 注意点 map循环的时候 不能够有空的 否则回出问题哦. var list= [{ ...
- 201871020225-牟星源《面向对象程序设计(java)》第十三周学习总结
201871020225-牟星源<面向对象程序设计(java)>第十三周学习总结 项目 内容 这个作业属于哪个课程 https://www.cnblogs.com/nwnu-daizh/ ...
- mybatis多对多关联查询
多对多关系 一个学生可以选多门课程,而一门课程可以由多个学生选择,这就是一个典型的多对多关联关系.所谓多对多关系,其实是由两个互反的一对多关系组成.即多对多关系都会通过一个中间表来建立,例如选课表.学 ...
- shell之seq
seq 用于生成从一个数到另一个数之间的所有整数 seq [选项]... 尾数 seq [选项]... 首数 尾数 seq [选项]... 首数 增量 尾数 例如: 1. -s 指定分隔符,默认分隔 ...
- day28 8_7 网络编程之tcp协议
一.socket模块 socket模块就是用来网络搭建的模块,socket也叫套接字. 创建网络连接,需要使用两个模块进行模拟,一个作为server服务器端,一个作为client客户端. 在服务器端, ...
- jmeter,学这些重点就可以了
前言 对测试来说,jmeter应该是大家使用较频繁的测试工具之一,因为其开源.免费.轻量.功能强大,支持很多种协议,除了测功能,还能做自动化和性能测试: 据某机构的调查,性能并发工具中,jmeter市 ...
- Shell 脚本中 '$' 符号的多种用法
通常情况下,在工作中用的最多的有如下几项: $0:Shell 的命令本身 $1 到 $9:表示 Shell 的第几个参数 $? :显示最后命令的执行情况 $#:传递到脚本的参数个数 $$:脚本运行的当 ...
- tornado模板的使用
一. 配置模板路径 settings中使用template_path来指定模板的路径, 实例化服务对象时加载进去即可. 二. 模板的使用 1. 使用self.render()方法可返回指定的html页 ...
- Codeforces Round #576 (Div. 1)
Preface 闲来无事打打CF,就近找了场Div1打打 这场感觉偏简单,比赛时艹穿的人都不少,也没有3000+的题 两三个小时就搞完了吧(F用随机水过去了) A. MP3 题意不好理解,没用翻译看了 ...