上周写了一个 适合初学者入门 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 并且重写了下面三个方法:

  1. attemptAuthentication(): 验证用户身份。
  2. successfulAuthentication() : 用户身份验证成功后调用的方法。
  3. 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 对需要权限才能访问的资源进行访问的时候,这个类是主要用到的,下面按照步骤来说一说每一步到底都做了什么。

  1. 当用户使用系统返回的 token 信息进行登录的时候 ,会首先经过doFilterInternal()方法,这个方法会从请求的 Header 中取出 token 信息,然后判断 token 信息是否为空以及 token 信息格式是否正确。
  2. 如果请求头中有 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 配置类中我们主要配置了:

  1. 密码编码器 BCryptPasswordEncoder(存入数据库的密码需要被加密)。
  2. AuthenticationManager 设置自定义的 UserDetailsService以及密码编码器;
  3. 在 Spring Security 配置指定了哪些路径下的资源需要验证了的用户才能访问、哪些不需要以及哪些资源只能被特定角色访问;
  4. 将我们自定义的两个过滤器添加到 Spring Security 配置中;
  5. 将两个自定义处理权限认证方面的异常类添加到 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 优缺点分析

优点

  1. 无状态,服务器不需要存储 Session 信息。
  2. 有效避免了CSRF 攻击。
  3. 适合移动端应用。
  4. 单点登录友好。

缺点

  1. 注销登录等场景下 token 还有效
  2. token 的续签问题

公众号

如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。

Spring Boot 使用 JWT 进行身份和权限验证的更多相关文章

  1. spring boot:spring security整合jwt实现登录和权限验证(spring boot 2.3.3)

    一,为什么使用jwt? 1,什么是jwt? Json Web Token, 它是JSON风格的轻量级的授权和身份认证规范, 可以实现无状态.分布式的Web应用授权 2,jwt的官网: https:// ...

  2. Spring Boot初识(4)- Spring Boot整合JWT

    一.本文介绍 上篇文章讲到Spring Boot整合Swagger的时候其实我就在思考关于接口安全的问题了,在这篇文章了我整合了JWT用来保证接口的安全性.我会先简单介绍一下JWT然后在上篇文章的基础 ...

  3. Spring Boot(十四):spring boot整合shiro-登录认证和权限管理

    Spring Boot(十四):spring boot整合shiro-登录认证和权限管理 使用Spring Boot集成Apache Shiro.安全应该是互联网公司的一道生命线,几乎任何的公司都会涉 ...

  4. Spring Boot Security JWT 整合实现前后端分离认证示例

    前面两章节我们介绍了 Spring Boot Security 快速入门 和 Spring Boot JWT 快速入门,本章节使用 JWT 和 Spring Boot Security 构件一个前后端 ...

  5. 基于Spring Cloud、JWT 的微服务权限系统设计

    基于Spring Cloud.JWT 的微服务权限系统设计 https://gitee.com/log4j/pig https://github.com/kioyong/spring-cloud-de ...

  6. 部署spring boot + Vue遇到的坑(权限、刷新404、跨域、内存)

    部署spring boot + Vue遇到的坑(权限.刷新404.跨域.内存) 项目背景是采用前后端分离,前端使用vue,后端使用springboot. 工具 工欲善其事必先利其器,我们先找一个操作L ...

  7. spring boot Shiro JWT整合

    一个api要支持H5, PC和APP三个前端,如果使用session的话对app不是很友好,而且session有跨域攻击的问题,所以选择了JWT 1.导入依赖包 <dependency> ...

  8. (转)Spring Boot (十四): Spring Boot 整合 Shiro-登录认证和权限管理

    http://www.ityouknow.com/springboot/2017/06/26/spring-boot-shiro.html 这篇文章我们来学习如何使用 Spring Boot 集成 A ...

  9. Spring Boot (十四): Spring Boot 整合 Shiro-登录认证和权限管理

    这篇文章我们来学习如何使用 Spring Boot 集成 Apache Shiro .安全应该是互联网公司的一道生命线,几乎任何的公司都会涉及到这方面的需求.在 Java 领域一般有 Spring S ...

随机推荐

  1. Python 简易的异步协程使用方法

    代码 import asyncio async def ex(id, n): print(id+" start") await asyncio.sleep(n/2) print(i ...

  2. uwsgi + nginx 发布

    下载uwsgi 基于pip 若是没有下载 yum install -y python2-pip pip install uwsgi 出上面的错 ,安装python的development包 yum i ...

  3. 201871010104-陈园园 《面向对象程序设计(java)》第十五周学习总结

    201871010104-陈园园 <面向对象程序设计(java)>第十五周学习总结 项目 内容 这个作业属于哪个课程 https://www.cnblogs.com/nwnu-daizh/ ...

  4. Druid(数据库连接池) 学习资料

    学习资料 网址 官方文档 https://github.com/alibaba/druid/wiki/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98 主流Java数据库连接池 ...

  5. MySQL数据库 外键,级联, 修改表的操作

    1.外键: 用来建立两张表之间的关系 - 一对多 - 多对多 - 一对一 研究表与表之间的关系: 1.定义一张 员工部门表 id, name, gender, dep_name, dep_desc - ...

  6. 【Spring AOP】AOP介绍(一)

    AOP(Aspect Oriented Programming) 面向切面编程,是Spring框架的一个重要组件. AOP应该算是对OOP(面向对象编程)的补充和完善.OOP引入封装.继承.多态等概念 ...

  7. 微信小程序 - 组件 | 自定义组件 | 组件事件传递页面

    组件 小程序允许我们使用自定义组件的方式来构建页面 类似Vue的小组件 自定义组件 类似于页面,一个自定义组件由 json, wxml, wxss, js 4个文件组成 1.创建 1.创建compon ...

  8. SGD的动量(Momentum)算法

    引入动量(Momentum)方法一方面是为了解决“峡谷”和“鞍点”问题:一方面也可以用于SGD 加速,特别是针对高曲率.小幅但是方向一致的梯度. 如果把原始的 SGD 想象成一个纸团在重力作用向下滚动 ...

  9. BZOJ练习记

    决定从头到尾干一波BZOJ!可能会写没几题就停下吧,但还是想学学新姿势啦. 1001. [BeiJing2006]狼抓兔子 即求 $(1, 1)$ 到 $(n, m)$ 的最小割.跑 dinic 即可 ...

  10. 13 opencv训练器

    https://blog.csdn.net/WZZ18191171661/article/details/91305466 https://blog.csdn.net/qq_25352981/arti ...