1 写在之前

本博客主要使用Spring Boot 整合Spring Security + JWT实现权限管理,利用JWT工具生成token,返回给登录接口。在访问其他接口时,采用Bearer Token的方式携带登录时获取的token进行验证,token验证通过,到达ctrl层的对应接口,验证失败,返回401错误。登录与验证的流程如下。

登录流程

接口验证流程 

接下来介绍最核心的继承WebSecurityConfigurerAdapter类的配置类SysSecurityConfig,然后我会根据登录流程,接口验证流程,登出流程依次讲解本系统部分代码,整个系统代码请到github获取。

2.系统总体概览

2.1 代码层次结构

上图展示了本项目的全部核心代码类, 除了经典的ctrl, service, model, dao层以外,项目还加入一些自定义的异常(PermissionDeniedException),封装的返回结果(BaseResponse<T>),自定义返回的状态码(ResultCode)以及各种handler处理器。

1.2 代码依赖配置

<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.1.RELEASE</version></parent><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><java.version>1.8</java.version><org.springframework.boot.version>2.3.1.RELEASE</org.springframework.boot.version><org.apache.common.lang3.version>3.12.0</org.apache.common.lang3.version><org.apache.common.beanutils.version>1.9.4</org.apache.common.beanutils.version><com.baomidou.mybatis-plus.version>3.4.3</com.baomidou.mybatis-plus.version><mysql.connector.java.version>8.0.12</mysql.connector.java.version><jjwt-version>0.9.0</jjwt-version><com.alibaba.fastjson.version>1.2.76</com.alibaba.fastjson.version><!--Lombok--><lombok.version>1.18.10</lombok.version><commons-io.version>2.6</commons-io.version><javadoc.version>3.0.0</javadoc.version><maven-release-plugin.version>2.5.3</maven-release-plugin.version></properties><dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>${org.springframework.boot.version}</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>${lombok.version}</version></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>${org.apache.common.lang3.version}</version></dependency><dependency><groupId>commons-beanutils</groupId><artifactId>commons-beanutils</artifactId><version>${org.apache.common.beanutils.version}</version></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>${com.baomidou.mybatis-plus.version}</version></dependency><!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>${mysql.connector.java.version}</version></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>${jjwt-version}</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>${com.alibaba.fastjson.version}</version></dependency></dependencies></dependencyManagement><dependencies><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>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId></dependency><dependency><groupId>commons-beanutils</groupId><artifactId>commons-beanutils</artifactId></dependency>//关于数据库的连接以及持久层框架本项目实际并没有用到<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId></dependency>//jwt工具库<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId></dependency></dependencies>

2.系统最核心配置代码讲解

2.1 Spring Security 权限控制核心配置类

/*** @program: authority-management-sys* @author: zgr* @create: 2021-07-25 20:52**/
@Configuration
@EnableWebSecurity
public class SysSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate final MyAuthenticationEntryPoint unauthorizedHandler;@Autowiredprivate final AccessDeniedHandler accessDeniedHandler;@Autowiredprivate final AuthenticationTokenFilter authenticationTokenFilter;@Autowiredprivate final SysLogoutHandler sysLogoutHandler;@Autowiredprivate final SysLogoutSuccessHandler sysLogoutSuccessHandler;@Autowiredprivate SysUserService sysUserService;@Autowiredpublic SysSecurityConfig(MyAuthenticationEntryPoint unauthorizedHandler,@Qualifier("RestAuthenticationAccessDeniedHandler") AccessDeniedHandler accessDeniedHandler, AuthenticationTokenFilter authenticationTokenFilter, SysLogoutHandler sysLogoutHandler, SysLogoutSuccessHandler sysLogoutSuccessHandler) {this.unauthorizedHandler = unauthorizedHandler;this.accessDeniedHandler = accessDeniedHandler;this.authenticationTokenFilter = authenticationTokenFilter;this.sysLogoutHandler = sysLogoutHandler;this.sysLogoutSuccessHandler = sysLogoutSuccessHandler;}/*** 解决 无法直接注入 AuthenticationManager** @return* @throws Exception*/@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}/*** 这里是对认证管理器的添加配置,添加自定义的用户查询服务** @param auth* @throws Exception*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(sysUserService).passwordEncoder(new BCryptPasswordEncoder());;}/*** 配置不需要安全验证的接口地址** @param web*/@Overridepublic void configure(WebSecurity web) {//配置允许匿名访问的接口,比如swagger地址,系统文档地址web.ignoring().antMatchers("/success/logout-page");}/*** 安全请求配置,这里配置的是security的部分,这里配置全部通过,安全拦截在资源服务的配置文件中配置,* 要不然访问未验证的接口将重定向到登录页面,前后端分离的情况下这样并不友好,无权访问接口返回相关错误信息即可** @param http* @return void*/@Overrideprotected void configure(HttpSecurity http) throws Exception {http.logout().addLogoutHandler(sysLogoutHandler).logoutSuccessHandler(sysLogoutSuccessHandler).and()// 由于使用的是JWT,我们这里不需要csrf.csrf().disable()// 权限不足处理类.exceptionHandling().accessDeniedHandler(accessDeniedHandler).and()// 认证失败处理类.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()// 基于token,所以不需要session.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 对于登录login要允许匿名访问.antMatchers(HttpMethod.POST, "/login").permitAll()// 访问接口的测试 需要拥有admin权限,实际环境中,可以配置在filter中进行权限的验证.antMatchers("/test/page").hasAuthority("admin")// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();// 禁用缓存http.headers().cacheControl();http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);}
}

2.2 jwt工具类

*** @program: authority-management-sys* @author: zgr* @create: 2021-07-25 21:06**/
@Component
@Slf4j
public class JwtTokenUtil {private Map<String, String> tokenMap = new ConcurrentHashMap<>(32);private Date generateExpirationDate(Long expiration) {return new Date(System.currentTimeMillis() + expiration);}/*** 生成令牌** @param userDetail 用户* @return 令牌*/public String generateAccessToken(SysUserDetails userDetail) {Map<String, Object> claims = generateClaims(userDetail);return generateAccessToken(userDetail.getUsername(), claims);}public boolean checkToken(String userName, String token) {/*1.token对比,是否存在*2.token是否过期,过期应该重新登录**/Long expirationTime = getExpirationTime(token);return userName != null&& tokenMap.containsKey(userName)&& tokenMap.get(userName).equals(token)&& expirationTime != null&& expirationTime > System.currentTimeMillis();}public void putToken(String userName, String token) {tokenMap.put(userName, token);}public void deleteToken(String userName) {tokenMap.remove(userName);}/*** 生成token** @param subject 用户名* @param claims* @return*/private String generateAccessToken(String subject, Map<String, Object> claims) {return generateToken(subject, claims);}/*** 根据token 获取用户信息** @param token* @return*/public SysUserDetails getUserDetails(String token) {SysUserDetails userDetail;try {final Claims claims = getClaims(token);Integer userId = Integer.parseInt(claims.get(Constant.CLAIM_KEY_USER_ID).toString());String username = getUsername(token);String roleName = claims.get(Constant.CLAIM_KEY_AUTHORITIES).toString();Role role = Role.builder().name(roleName).build();userDetail = new SysUserDetails(userId, username, null, role);log.info("user details {}", userDetail.toString());} catch (Exception e) {log.error("获取用户详情出错", e);userDetail = null;}return userDetail;}/*** 生成token** @param subject 用户名* @param claims  声明* @return*/private String generateToken(String subject, Map<String, Object> claims) {return Jwts.builder().setClaims(claims)// sub(Subject):代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。.setSubject(subject)// 设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。.setId(UUID.randomUUID().toString())// iat: jwt的签发时间.setIssuedAt(new Date())//有效时长.setExpiration(generateExpirationDate(Constant.ACCESS_TOKEN_EXPIRATION))//压缩格式.compressWith(CompressionCodecs.DEFLATE)//secret在实际使用中可以做成配置项.signWith(Constant.SIGNATURE_ALGORITHM, Constant.JWT_SECRET).compact();}/*** 根据token 获取用户名** @param token* @return*/public String getUsername(String token) {String username;try {final Claims claims = getClaims(token);username = claims.getSubject();} catch (Exception e) {log.error("获取用户名出错", e);username = null;}return username;}public Long getExpirationTime(String token) {Long expirationTime;try {final Claims claims = getClaims(token);expirationTime = claims.getExpiration().getTime();} catch (Exception e) {expirationTime = null;}return expirationTime;}/*** 根据token 获取用户ID** @param token* @return*/private Integer getUserId(String token) {Integer userId;try {final Claims claims = getClaims(token);userId = Integer.parseInt((String) claims.get(Constant.CLAIM_KEY_USER_ID));} catch (Exception e) {userId = null;}return userId;}/**** 解析token 信息* @param token* @return*/private Claims getClaims(String token) {Claims claims;try {claims = Jwts.parser().setSigningKey(Constant.JWT_SECRET).parseClaimsJws(token).getBody();} catch (Exception e) {claims = null;}return claims;}private Map<String, Object> generateClaims(SysUserDetails userDetail) {Map<String, Object> userDetails = new HashMap<>(16);userDetails.put(Constant.CLAIM_KEY_USER_ID, userDetail.getId());userDetails.put(Constant.CLAIM_KEY_AUTHORITIES, authoritiesToArray(userDetail.getAuthorities()).get(0));return userDetails;}private List authoritiesToArray(Collection<? extends GrantedAuthority> authorities) {List<String> list = new ArrayList<>();for (GrantedAuthority ga : authorities) {list.add(ga.getAuthority());}return list;}}

3.登录流程代码详解

登录流程文字描述:开启登录--->进入AuthenticationTokenFilter(直接跳过)--->进入ctrl层的LoginController中的login方法--->进入service逻辑层,依据用户名查找用户,组装用户详情,生成对应token--->接口返回token。ctrl层的没有任何逻辑,所以本节只展示比较重要的AuthenticationTokenFilter以及逻辑层的代码。

3.1 过滤器AuthenticationTokenFilter

此处说一下个人理解,AuthenticationTokenFilter是所有的访问接口的请求都会经过的,一般的接口,filter过滤通过,不能生成对应的Authentication放入SecurityContextHolder.getContext().setAuthentication()中,且在2系统最核心配置代码讲解中没有允许其匿名访问,那么接口就会返回权限不足的信息。

/*** @program: authority-management-sys* @author: zgr* @create: 2021-07-26 12:02**/@Slf4j
@Component
public class AuthenticationTokenFilter extends OncePerRequestFilter {@Autowiredprivate JwtTokenUtil jwtTokenUtil;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)throws ServletException, IOException {if (!request.getRequestURL().toString().contains(Constant.LOGIN_URL)) {//取出tokenString token = request.getHeader(Constant.TOKEN_HEADER);if (StringUtils.isNotEmpty(token) && token.startsWith(Constant.TOKEN_STARTER)) {token = token.substring(Constant.TOKEN_STARTER.length()).trim();} else {token = null;}//关于token形式验证通过,验证token的内容String username = jwtTokenUtil.getUsername(token);if (username != null && jwtTokenUtil.checkToken(username, token) && SecurityContextHolder.getContext().getAuthentication() == null) {log.info("{} access the {} API", username, request.getRequestURL());SysUserDetails userDetails = jwtTokenUtil.getUserDetails(token);UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));log.info(String.format("Authenticated userDetail %s, setting security context", username));SecurityContextHolder.getContext().setAuthentication(authentication);}}chain.doFilter(request, response);}
}

3.2 service查找用户生成token

/*** @program: authority-management-sys* @author: zgr* @create: 2021-07-26 10:20**/@Service
@Slf4j
public class LoginServiceImpl implements LoginService {@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate JwtTokenUtil jwtTokenUtil;@Overridepublic String loginSys(LoginUserEntity loginUser) {final Authentication authentication = authenticate(loginUser.getUsername(), loginUser.getPassword());//存储认证信息SecurityContextHolder.getContext().setAuthentication(authentication);//生成tokenlog.info("{} 登录,生成token", loginUser.getUsername());final SysUserDetails userDetail = (SysUserDetails) authentication.getPrincipal();final String token = jwtTokenUtil.generateAccessToken(userDetail);//存储tokenjwtTokenUtil.putToken(loginUser.getUsername(), token);return token;}private Authentication authenticate(String username, String password) {try {// 该方法会去调用userDetailsService.loadUserByUsername()去验证用户名和密码,// 如果正确,则存储该用户名密码到security 的 context中return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));} catch (DisabledException | BadCredentialsException e) {throw new PermissionDeniedException("用户名或密码错误,请重新登录");}}
}

3.3 依据用户名查找用户,生成对应UserDetails

@Service
@Slf4j
public class SysUserServiceImpl implements SysUserService {@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {log.info("根据用户名{}查询用户信息", username);//此处实际可以从数据库中查找对应的用户名User user;if (username.equals("admin")) {user = User.builder().id(1).username("admin").password("$2a$10$blrIf6.vDYUAGbq.8fk2heScZYVgMl8lFAUWvPi1aZ9aiCar3pALe").test("this is test").build();}else {throw new UsernameNotFoundException(username + "不存在");}Role role = Role.builder().id(1).name("admin").build();//这里权限列表,这个为方便直接下(实际开发中查询用户时连表查询出权限)return new SysUserDetails(user.getId(), user.getUsername(), user.getPassword(), role);}
}

3.4 登录结果展示

3.4.1 正确登录结果展示

3.4.2 用户名错误登录结果展示

3.4.3 密码错误登录结果展示

4.接口权限验证

访问接口--->进入AuthenticationTokenFilter,1.验证token的有效性,2.从token中取出信息组装成Authentication放入SecurityContextHolder.getContext().setAuthentication()中,验证通过--->访问对应ctrl层中的接口。

4.1 ctrl层中的接口

 @GetMapping("/test/page")public BaseResponse<String> testPage() {return BaseResponse.success("test page");}

4.2 验证结果展示

4.2.1 正确验证结果展示

4.2.2 不带token验证结果展示

4.2.3 token错误验证结果展示

5 退出系统

通过Spring Security 自带的LogoutFilter来执行,通过自定义的继承LogoutHandler类SysLogoutHandler处理登出逻辑,通过自定义的继承LogoutSuccessHandler类SysLogoutSuccessHandler实现登出成功后的逻辑。

5.1继承LogoutHandler类SysLogoutHandler

/*** @program: authority-management-sys* @author: zgr* @create: 2021-07-29 10:46**/
@Configuration
@Slf4j
public class SysLogoutHandler implements LogoutHandler {@Autowiredprivate JwtTokenUtil jwtTokenUtil;@Overridepublic void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {//是用户登出具体逻辑的实现,可以记录用户下线的时间,ip,以下为删除token的逻辑String token = request.getHeader(Constant.TOKEN_HEADER).substring(Constant.TOKEN_STARTER.length());String username = jwtTokenUtil.getUsername(token);if (username == null) {throw new PermissionDeniedException(ResultCode.UN_AUTHORIZED);}jwtTokenUtil.deleteToken(username);}
}

5.2 继承LogoutSuccessHandler类SysLogoutSuccessHandler

/*** @program: authority-management-sys* @author: zgr* @create: 2021-07-29 11:08**/
@Configuration
@Slf4j
public class SysLogoutSuccessHandler implements LogoutSuccessHandler {@Overridepublic void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {//登出成功的执行的逻辑ResultUtil.writeResponse(httpServletResponse, ResultCode.SUCCESS, ResultCode.SUCCESS.getMsg());}
}

5.3 登出结果展示

登录系统获取token--->访问验证接口--->退出系统--->再次访问验证接口

5.3.1 登录系统获取token结果展示

5.3.2 访问验证接口结果展示

5.3.3 登出系统结果展示

 5.3.4 再次访问验证接口展示

6 写在最后

以上只是对系统关键代码的一个展示,有很多引用的工具类代码,最关键的token的生成验证以及从token中获取用户信息没有展示,想要代码可以找到最后的GitHub地址进行代码的clone。

代码目前还是实验阶段,离生产应用还有一段距离,比如用户查找没有与当下流行的基于角色的权限管理系统(RBAC)结合起来。在token的存储上,后期可以放在redis中进行缓存,微服务系统在进行token权限验证的时候直接访问redis做验证。

本文是对自己学习经验的一个总结,总觉得能写出来的东西才是自己掌握的东西,有很多不对或遗漏之处请指出,不尽感激。

7 引用

1.Spring Security(一)--Architecture Overview

2.Spring Security(二)--Guides

3.Spring Security(三)--核心配置解读

4.Spring Security(四)--核心过滤器源码分析

5.Spring Security(五)--动手实现一个IP_Login

6.Spring Boot整合实战Spring Security JWT权限鉴权系统

8 本文对应的GitHub地址

GitHub - airhonor/authority-management-sys: 基于spring-security和jwt实现基于角色的权限管理系统

Spring Security + JWT实现权限管理相关推荐

  1. 用Spring Security做分布式权限管理 - 卷一基本功

    我们但凡做一个系统,这个系统不是在封闭环境中,不是只给一个人用,为了保证系统与数据安全,那么就会涉及到权限控制,权限控制这个东西可以说是很多系统的基础,因为我们不能让所有人对系统上的所有资源都进行同样 ...

  2. springboot jwt token前后端分离_基于Spring Boot+Spring Security+JWT+Vue前后端分离的开源项目...

    一.前言 最近整合Spring Boot+Spring Security+JWT+Vue 完成了一套前后端分离的基础项目,这里把它开源出来分享给有需要的小伙伴们 功能很简单,单点登录,前后端动态权限配 ...

  3. 超实用,Spring Security+JWT+Vue实现一个前后端分离无状态认证Demo

    作者: 陕西颜值扛把子 https://zhuanlan.zhihu.com/p/95560389 精彩推荐 一百期Java面试题汇总 SpringBoot内容聚合 IntelliJ IDEA内容聚合 ...

  4. Spring boot 整合Spring Security Jwt

    记录学习Spring boot 整合Spring Security Jwt 学习参考 – 慢慢的干货 https://shimo.im/docs/OnZDwoxFFL8bnP1c/read 首先创建S ...

  5. Spring Boot + Spring Security + JWT + 微信小程序登录

    Spring Boot + Spring Security + JWT + 微信小程序登录整合教程 参考文章 文章目录 整合思想 整合步骤 1. AuthenticationToken 2. Auth ...

  6. spring security+jwt 登录认证

    spring security+jwt 登录认证 1.综述 2.版本与环境 3.架构 4.数据库认证逻辑图 5.案例 security+jwt 5.1引入依赖 5.2新建工具类 5.2新建组件类 5. ...

  7. Springboot Spring Security +Jwt+redis+mybatisPlus 动态完成 前后端分离认证授权

    Springboot Spring Security +Jwt 动态完成 前后端分离认证授权 文章目录 Springboot Spring Security +Jwt 动态完成 前后端分离认证授权 前 ...

  8. Angular 6集成Spring Boot 2,Spring Security,JWT和CORS

    主要内容:Spring Boot 2的基础应用.CORS配置.Actuator监控:Spring Boot集成springfox-swagger,利用Swagger生成JSON API文档,利用Swa ...

  9. Java开发 - 单点登录初体验(Spring Security + JWT)

    目录​​​​​​​ 前言 为什么要登录 登录的种类 Cookie-Session Cookie-Session-local storage JWT令牌 几种登陆总结 用户身份认证与授权 创建工程 添加 ...

最新文章

  1. 生成浮点数列表:Python range():TypeError: ‘float‘ object cannot be interpreted as an integer
  2. java singleton 多线程_Java创建线程安全的单例singleton
  3. 五分钟带你入门TensorFlow
  4. 二进制在计算机电路中得到广泛的应用,电子技术与单片机的发展应用2喜欢就下吧(全文完整版)...
  5. WPF实现实现圆形菜单
  6. 笑脸喜迎新同学,热情送给新伙伴
  7. java jvm虚拟机_Java虚拟机(JVM)简介
  8. 408计算机先学哪个,408计算机统考各科难度分析
  9. Objective-C语法快速参考
  10. python代替shell脚本_自动化shell脚本except与python的pexpect模块
  11. JQuery------jQuery.parseHTML()的使用方法
  12. 2020 年 9 月程序员工资统计,新出炉!
  13. Python 装逼手机号码方法 低配版 map方法解析
  14. 试读《线上幽灵:世界头号黑客米特尼克自传》
  15. 格物致知诚意正心修身齐家治国平天下是什么意思【转载】
  16. 牛客练习赛47 B:DongDong认亲戚 (并查集)
  17. android 7.0 root工具,KingRoot全球率先实现Android 7.0一键 Root
  18. 杭州证历本如何使用_药店也可以用
  19. GBase 8c产品简介
  20. ――关于几个著名小说的胡思乱想

热门文章

  1. updating java index_myeclipse右下角的updating indexes 是什么意思?
  2. 计算机核心显卡,电脑核心组件之显卡如何选择
  3. 8.中学班级管理与教师心理
  4. (裴蜀定理)ax + by = m 有解,当且仅当 m 是 gcd(a,b) 的倍数
  5. P2327 [SCOI2005]扫雷 - 模拟
  6. vscode快速生成代码块
  7. LOAM源码解析1一scanRegistration
  8. python编写淘宝秒杀脚本
  9. Standardized QCI characteristics
  10. css代码上一章 下一章,第一章、css和文档