文章目录

  • 系列目录
  • 前言
  • 一、无状态登录
  • 二、JWT介绍
    • 1、什么是jwt
      • 头部(Header)
      • 载荷(Payload)
      • 签名(Signature)
    • 2、JWT工作流程
    • 3、简单实现
  • 三、整合JWT
    • 后端实现
    • 修改Swagger配置
    • 前端适配

系列目录

SpringSecurity权限管理系统实战—一、项目简介和开发环境准备
SpringSecurity权限管理系统实战—二、日志、接口文档等实现
SpringSecurity权限管理系统实战—三、主要页面及接口实现
SpringSecurity权限管理系统实战—四、整合SpringSecurity(上)
SpringSecurity权限管理系统实战—五、整合SpringSecurity(下)
SpringSecurity权限管理系统实战—六、SpringSecurity整合JWT
SpringSecurity权限管理系统实战—七、处理一些问题
SpringSecurity权限管理系统实战—八、AOP 记录用户日志、异常日志
SpringSecurity权限管理系统实战—九、数据权限的配置

前言

最近是真的懒,感觉我每个月都有那么几天什么都不想干。。

画风一转,前几天的lpl忍界大战是真的精彩,虚假的电竞春晚:RNG vs IG 。真正的电竞春晚 TES vs IG。TES自从阿水和kasra加入之后,状态直接起飞,在我看来TES将是s10夺冠热门之一。不过这一次木叶村战胜了晓组织。

本以为会打满三局,没想到ig直接2:0带走。rookie线上压制了新皇knight,确实永远可以相信宋义进,或许是因为‍小钰采访吧。

这两把我最没想到的是kasra被宁王压着打,几乎没有节奏,宝蓝在哪都是阿水的噩梦。这波啊,这波是盗版打赢了正版,puff小小的证明了自己。

最后还是希望lpl的饭圈粉少一点,peace

进入正题

一、无状态登录

  • 有状态登录

    我们知道在原始的项目中我们是通过session和cookie来实现用户的识别认证。但是这样做无疑会增加服务器的压力,服务的保存了大量的数据。如果业务需要扩展,搭建了集群的话,还需要将session共享。

  • 无状态登录

    而什么是无状态登录呢,简而言之,就是服务的不需要再保存任何的用户信息,而是用户自己携带者信息去访问服务端,服务端通过这些信息来识别客户端身份。这样一来,有状态登录的缺点都被解决了,但是这同样也会带来新问题。比如token信息无法在服务端注销,必须要等其自己过期,占用更多的空间(意味着需要更多带宽),修改密码后原本的token在没过期时仍然可用访问系统等。

二、JWT介绍

1、什么是jwt

JWT是 Json Web Token 的缩写。它是基于 RFC 7519 标准定义的一种可以安全传输的 小巧 和 自包含 的JSON对象。由于数据是使用数字签名的,所以是可信任的和安全的。JWT可以使用HMAC算法对secret进行加密或者使用RSA的公钥私钥对来进行签名。

我们来看一下jwt长什么样

eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJjb2Rlcm15IiwiYXV0aG9yaXRpZXMiOiJhZG1pbiIsImV4cCI6MTU5NjA4MDM5OX0.rfDtzMus50uAFnqMw1tm3c_ZYbmUNkIRqMkeJ0510PAH-RCUWtZkfNPTDYAGVVfDU6jmdEkGyNYvGy3UrNq5pA

JSON Web 令牌以紧凑的形式由三个部分组成,由点分隔,它们包括:

  • 头部
  • 负载
  • 签名

头部(Header)

jwt的头部承载两部分信息:

  • 声明类型,这里是jwt
  • 声明加密的算法 通常直接使用 HMAC SHA256

像这样

{'typ': 'JWT','alg': 'HS256'
}

载荷(Payload)

这个部分用来承载要传递的数据,他的默认字段有

  • iss:发行人
  • exp:到期时间
  • sub:主题
  • aud:用户
  • nbf:在此之前不可用
  • iat:发布时间
  • jti:JWT ID用于标识该JWT

除以上默认字段外,我们还可以自定义私有字段,例如

{"sub": "1234567890","name": "John Doe","admin": true
}

签名(Signature)

Signature 部分是对前两部分的签名,防止数据篡改。

2、JWT工作流程

  • 用户发起登录请求
  • 服务端验证身份,将用户信息,标识等信息打包成jwt token返回给客户端
  • 用户拿到token,携带token发送请求给服务端
  • 服务的验证token是否可用,可用便根据其y业务逻辑返回相应结果。

3、简单实现

首先我们在maven中引入以下依赖

      <!--jjwt--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency>

新建JwtTest来测试一下

/*** @author codermy* @createTime 2020/7/30*/
public class JwtTest {public static void main(String[] args) {String token = Jwts.builder()//用户名.setSubject("codermy")//自定义属性 放入用户拥有请求权限.claim("authorities","admin")// 设置失效时间为1分钟.setExpiration(new Date(System.currentTimeMillis()+1000*60))// 签名算法和密钥.signWith(SignatureAlgorithm.HS512, "java").compact();System.out.println(token);}

输出

eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJjb2Rlcm15IiwiYXV0aG9yaXRpZXMiOiJhZG1pbiIsImV4cCI6MTU5NjA4MDM5OX0.rfDtzMus50uAFnqMw1tm3c_ZYbmUNkIRqMkeJ0510PAH-RCUWtZkfNPTDYAGVVfDU6jmdEkGyNYvGy3UrNq5pA

我们再来解析

 //解析tokenClaims claims = Jwts.parser().setSigningKey("java").parseClaimsJws(token).getBody();System.out.println(claims);//获取用户名String username = claims.getSubject();System.out.println("username:"+username);//获取权限String authority = claims.get("authorities").toString();System.out.println("权限:"+authority);System.out.println("到期时间:" + claims.getExpiration());

输出

{sub=codermy, authorities=admin, exp=1596082316}
username:codermy
权限:admin
到期时间:Thu Jul 30 12:11:56 CST 2020

三、整合JWT

后端实现

其实jwt本身很好理解,无非就就是一把钥匙,可用打开对应的锁,这不过这把钥匙稍微特殊点,它还带了主人的一些信息。难理解的是要将它符合业务逻辑的整合进框架中。我自己就被绕了好久才明白。

我这里写了一个Jwt的工具类,用于生成和解析jwt

/*** @author codermy* @createTime 2020/7/23*/
@Component
public class JwtUtils {private static final String CLAIM_KEY_USERNAME = "sub";private static final String CLAIM_KEY_CREATED = "created";@Value("${jwt.secret}")private String secret;@Value("${jwt.expiration}")private  Long expiration;// 创建tokenpublic  String generateToken(String username) {return Jwts.builder().signWith(SignatureAlgorithm.HS512, secret).setSubject(username).setIssuedAt(new Date()).setExpiration(new Date(System.currentTimeMillis() + expiration * 1000)).compact();}// 从token中获取用户名public  String getUserNameFromToken(String token){return getTokenBody(token).getSubject();}// 是否已过期public  boolean isExpiration(String token){return getTokenBody(token).getExpiration().before(new Date());}private  Claims getTokenBody(String token){return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();}}

然后我们可以将jwt的一些信息写在yml中,使得可以灵活的配置。application.yml中添加如下配置

jwt:tokenHeader: Authorization #JWT存储的请求头secret: my-springsecurity-plus #JWT加解密使用的密钥expiration: 604800#JWT的超期限时间(60*60*24*7)tokenHead: 'Bearer ' #JWT负载中拿到开头,空格别忘了

我们照着jwt的工作流程来,首先是登录成功后客户端会返回一个jwt token

所以我们首先自定义一个MyAuthenticationSuccessHandler继承AuthenticationSuccessHandler,这是登录成功后的处理器

/*** @author codermy* @createTime 2020/8/1* 登录成功*/
@Component
@Slf4j
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {@Autowiredprivate JwtUtils jwtUtils;@Value("${jwt.tokenHeader}")private String tokenHeader;@Value("${jwt.tokenHead}")private String tokenHead;@Overridepublic void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {JwtUserDto userDetails = (JwtUserDto)authentication.getPrincipal();//拿到登录用户信息String jwtToken = jwtUtils.generateToken(userDetails.getUsername());//生成tokenResult result = Result.ok().message("登录成功").jwt(jwtToken);System.out.println(JSON.toJSONString(result));//用于测试httpServletResponse.setCharacterEncoding("utf-8");//修改编码格式httpServletResponse.setContentType("application/json");httpServletResponse.getWriter().write(JSON.toJSONString(result));//输出结果httpServletResponse.sendRedirect("/api/admin");//重定向到api/admin页面。我这里路由名取的不是很好}
}

然后我们再写一个jwt的拦截器,让每个请求都需要验证jwt token

/*** @author codermy* @createTime 2020/7/30*/
@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {@Autowiredprivate UserDetailsServiceImpl userDetailsService;@Autowiredprivate JwtUtils jwtUtils;@Value("${jwt.tokenHeader}")private String tokenHeader;@Value("${jwt.tokenHead}")private String tokenHead;@Overrideprotected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain chain) throws ServletException, IOException {String authHeader = request.getHeader(this.tokenHeader);//拿到requset中的headif (authHeader != null && authHeader.startsWith(this.tokenHead)) {String authToken = authHeader.substring(this.tokenHead.length());// The part after "Bearer "String username = jwtUtils.getUserNameFromToken(authToken);//解析token获取用户名log.info("checking username:{}", username);if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);if (userDetails != null) {//判断是否存在这个给用户UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));log.info("authenticated user:{}", username);SecurityContextHolder.getContext().setAuthentication(authentication);}}}chain.doFilter(request, response);}}

这里为了之后结果更直观,自定义一个AuthenticationEntryPoint,用于在未登录是访问接口返回json而不是login.html

/*** @author codermy* @createTime 2020/8/1* 当未登录或者token失效访问接口时,自定义的返回结果*/
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {response.setCharacterEncoding("UTF-8");//设置编码格式response.setContentType("application/json");response.getWriter().println(JSON.toJSONString(Result.error().message("尚未登录,或者登录过期   " + authException.getMessage())));response.getWriter().flush();}
}

将上述方法加入到SpringSecurityConfig中

/*** @author codermy* @createTime 2020/7/15*/
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate UserDetailsServiceImpl userDetailsService;@Autowiredprivate VerifyCodeFilter verifyCodeFilter;@AutowiredMyAuthenticationSuccessHandler authenticationSuccessHandler;@Autowiredprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;@Autowiredprivate RestAuthenticationEntryPoint restAuthenticationEntryPoint;@Autowiredprivate RestfulAccessDeniedHandler accessDeniedHandler;@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder(12);}/*** 身份认证接口*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());}@Overridepublic void configure(WebSecurity web) throws Exception {web.ignoring().antMatchers(HttpMethod.GET,"/swagger-resources/**","/PearAdmin/**","/**/*.html","/**/*.css","/**/*.js","/swagger-ui.html","/webjars/**","/v2/**");//放行静态资源}/*** anyRequest          |   匹配所有请求路径* access              |   SpringEl表达式结果为true时可以访问* anonymous           |   匿名可以访问* denyAll             |   用户不能访问* fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)* hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问* hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问* hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问* hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问* hasRole             |   如果有参数,参数表示角色,则其角色可以访问* permitAll           |   用户可以任意访问* rememberMe          |   允许通过remember-me登录的用户访问* authenticated       |   用户登录后可访问*/@Overrideprotected void configure(HttpSecurity http) throws Exception {http.addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class);http.csrf().disable()//关闭csrf.sessionManagement()// 基于token,所以不需要session.sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().httpBasic().authenticationEntryPoint(restAuthenticationEntryPoint)//未登陆时返回 JSON 格式的数据给前端,否则是html.and().authorizeRequests().antMatchers("/captcha").permitAll()//任何人都能访问这个请求.anyRequest().authenticated().and().formLogin().loginPage("/login.html")//登录页面 不设限访问.loginProcessingUrl("/login")//拦截的请求.successHandler(authenticationSuccessHandler) // 登录成功处理器.permitAll()// 防止iframe 造成跨域.and().headers().frameOptions().disable().and();// 禁用缓存http.headers().cacheControl();// 添加JWT拦截器http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);}

我这里直接贴了完整的代码,因为有添加也有删除,不是很好描述,大家对比着之前的来看,都添加了注释。

现在我们重启项目,用admin账号来登录。登录成功后发现页面并没有跳转到我们想去的页面,但是控制台打印出了我们想要的jwt信息

{"code":200,"data":[],"jwt":"eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImlhdCI6MTU5NjI1OTgyOCwiZXhwIjoxNTk2ODY0NjI4fQ.Khn5t6WjOsuG6R2if1Q_gAeNq-zTamIAO32b1UVc6L8-6_IAHMaCeWr-v7H2-7Hob0SSmmK23dv71_da-YK8hw","msg":"登录成功","success":true}

这是为什么呢?

着很好理解,因为我们的jwt拦截器已经起了作用,而我们原本的前端页面是没有把jwt token添加在header上的,所以认为没有登录,重定向到了登录页面。

但是我们现在可以借助postman来测试,postman是一个测试api的工具,大家可以自行百度,这里不做过多介绍。

在我们未携带jwt token信息时,访问http://localhost:8080/api/menu接口,就会报如下错误

我们在header中添加上,之前登录成功控制台打印的token信息(因为我们添加了图片验证码,所以登录不是很方便用postman,我们可以在浏览器中登录或者先把验证码的拦截器去除)

加上了token信息之后再去访问http://localhost:8080/api/menu接口,发现已经可以正常访问了

我们再尝试用test用户登录后获取到jwt token访问该接口,会报如下错误

修改Swagger配置

直接贴代码

/*** @author codermy* @createTime 2020/7/10*/
@Configuration
@EnableSwagger2
public class SwaggerConfig {@Value("${jwt.tokenHeader}")private String tokenHeader;@Value("${jwt.tokenHead}")private String tokenHead;@Beanpublic Docket createRestApi() {ParameterBuilder ticketPar = new ParameterBuilder();List<Parameter> pars = new ArrayList<>();ticketPar.name(tokenHeader).description("token").modelRef(new ModelRef("string")).parameterType("header").defaultValue(tokenHead + " ").required(true).build();pars.add(ticketPar.build());return new Docket(DocumentationType.SWAGGER_2).apiInfo(webApiInfo()).select().apis(RequestHandlerSelectors.basePackage("com.codermy.myspringsecurityplus.controller")).paths(PathSelectors.any()).paths(Predicates.not(PathSelectors.regex("/error.*"))).build().globalOperationParameters(pars);}/*** 该套 API 说明,包含作者、简介、版本、等信息* @return*/private ApiInfo webApiInfo(){return new ApiInfoBuilder().title("my-springsecurity-plus-API文档").description("本文档描述了my-springsecurity-plus接口定义").version("1.0.5").build();}}

现在再swagger中就可以添加token测试了

前端适配

那么我们现在已经简单的实现了jwt的无状态登录功能,需要做的就是让前端的请求都带上jwt token。

。。。研究了半天没弄懂,所以暂时先搁置,下一章解决它。有知道怎么设置请求头的小伙伴也可以留言告诉我

所以本章结束的代码是不能正常在浏览器运行的,但是可以在postman和swagger中测试(如果想运行,在SpringSecurityConfig中添加上.rememberMe()即可)

在github和gitee中可获取源代码,与本系列文章同步更新

SpringSecurity权限管理系统实战—六、SpringSecurity整合JWT相关推荐

  1. SpringSecurity权限管理系统实战—一、项目简介和开发环境准备

    源码获取: github或者gitee 文章目录 系列目录 前言 一.简介 二.什么是RBAC 三.系统功能 四.环境搭建 五.技术栈 六.说明 七.项目截图 八.请作者喝杯卡布奇诺 系列目录 Spr ...

  2. SpringSecurity权限框架实战

    pom文件: <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http ...

  3. SpringSecurity权限管理框架系列(六)-Spring Security框架自定义配置类详解(二)之authorizeRequests配置详解

    1.预置演示环境 这个演示环境继续沿用 SpringSecurit权限管理框架系列(五)-Spring Security框架自定义配置类详解(一)之formLogin配置详解的环境. 2.自定义配置类 ...

  4. SpringSecurity权限管理框架系列(七)-SpringSecurity自定义配置类中自定义Filter的使用详解

    1.Filter请求过滤器 filter请求过滤器可以帮助我们进行HttpServletRequest请求和HttpServletResponse响应的过滤 在自定义的Filter过滤器中我们可以对我 ...

  5. Spring Security + SpringBoot + Mybatis-plus实现前后端分离的权限管理系统

    碎碎念 在学习Spring Security的时候,有收集到这样一张图,感觉描述还是很详尽的.有阅读了一下源码,个人理解,Spring Security默认对POST的/login请求做出响应,然后就 ...

  6. ABP+AdminLTE+Bootstrap Table权限管理系统一期

      初衷    学而时习之,不亦说乎,温顾温知新,可以为师矣.           看懂远不如动手去做,动手做才能发现很多自己不懂的问题,不断的反思和总结,"乐于分享是一种境界的突破&quo ...

  7. 基于MVC4+EF5+EasyUI技术实现通用权限管理系统(EpPlus、HignCharts、Reportviewer报表)...

    基于MVC4+EF5+EasyUI技术实现通用权限管理系统(EpPlus.HignCharts.Reportviewer报表) 适合人群:高级 课时数量:150课时 用到技术:MVC.EF.T4.Lo ...

  8. SpringSecurity - 整合JWT使用 Token 认证授权

    一.SpringSecurity 前面讲解了SpringSecurity的动态认证和动态权限角色,我们都知道在现在大多都是微服务前后端分离的模式开发,前面讲的还是基于Session的,本篇我们整合JW ...

  9. SpringSecurity 整合 JWT

    项目集成Spring Security(一) 在上一篇基础上继续集成 JWT ,实现用户身份验证. 前言 前后端分离项目中,如果直接把 API 接口对外开放,我们知道这样风险是很大的,所以在上一篇中我 ...

最新文章

  1. 指针,引用之间的关系
  2. LeetCode 14. Longest Common Prefix字典树 trie树 学习之 公共前缀字符串
  3. idea14创建java项目_使用IntelliJ IDEA 14和Maven创建java web项目
  4. 5.13 卡尔曼滤波
  5. 使用静态代理模式实现公用的报表导出功能
  6. javascript 对象基础 继承机制实例【对象冒充】
  7. 测试用例设计--判定表
  8. java pc端软件抓包,如何通过抓包工具fiddler获取java程序的http请求
  9. BZOJ.3261.最大异或和(可持久化Trie)
  10. python.exe无法找到入口
  11. 可取回的国内csgo开箱网站incsgo开箱
  12. SpringBoot+海康威视摄像头实现在前端的预览
  13. Bootstrap carousel轮转图的使用
  14. 在空间绘制出一系列螺旋上升的点
  15. 【Oracle数据库】关联、子查询
  16. 软件的生命周期(软件工程各阶段的工作)
  17. vmware workstation中搭建云平台,虚拟机重启遇到的问题
  18. 让世界充满AI—时代的开拓者(程序员)
  19. 如何合并多个excel文件图文步骤
  20. 笔记本电脑,充电器一拔立马关机,突然无法用电池怎么办

热门文章

  1. 跟领导碰面不知道该说什么,给你几个技巧来破冰?
  2. CF1474-A. Puzzle From the Future
  3. [生存志] 第63节 樊迟司马耕
  4. 低成本有效激励员工五大法则
  5. 公众号如何让更多人看到?这三种方法超有效!
  6. 微软Windows帝国幕后的10大关键人物
  7. 法国龙凤胎洗澡睡着 紧抱对方相互依偎(图)
  8. python学习——相关软件汇总
  9. 关闭windows自带杀毒软件Windows defender命令,命令行关闭windows服务
  10. oracle导入导出