这篇博客是在我上篇发的 SpringBoot+Shiro+Redis+Mybatis-plus 实战项目 之上添加了JWT认证和前后端分离,所以这篇博客重点是贴出 JWT 学习总结的代码,希望可以帮助到大家!
JWT 全称就是 JSON WEB TOKEN,可以看作是一个获得请求资格的令牌,我们有了这个令牌,才可以访问到网站的大部分功能(接口)。
JWT 分成三段
以 . 号连接,有点像 IP 地址的格式
例如: header.payload.sign
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.15.0</version> </dependency>
package com.jmu.shiro_demo.utils; import com.auth0.jwt.JWT; import com.auth0.jwt.JWTCreator; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.DecodedJWT; import java.security.Signature; import java.util.Calendar; import java.util.Map; public class JWTutil { private static final String SIGN = "jiachengren"; //JWT 签名 private static final int DEFAULT_JWT_EXPIRE_DAYS = 7; //默认JWT过期天数 public static String getToken(Map<String,String> map) { JWTCreator.Builder builder = JWT.create(); Calendar instance = Calendar.getInstance(); instance.add(Calendar.DATE,DEFAULT_JWT_EXPIRE_DAYS); //1.header 默认 //2.payload 遍历 map map.forEach((k,v) -> { builder.withClaim(k,v); }); //3.设置过期时间和签名后生成 token String token = builder .withExpiresAt(instance.getTime()) .sign(Algorithm.HMAC256(SIGN)); return token; } public static DecodedJWT verify(String token){ return JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token); } }
package com.jmu.shiro_demo.intercepetor; import com.auth0.jwt.exceptions.AlgorithmMismatchException; import com.auth0.jwt.exceptions.SignatureVerificationException; import com.auth0.jwt.exceptions.TokenExpiredException; import com.fasterxml.jackson.databind.ObjectMapper; import com.jmu.shiro_demo.utils.JWTutil; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.HashMap; import java.util.Map; public class JWTIntercepetor implements HandlerInterceptor { private final String TOKEN_NAME = "token"; //JWT拦截器,所有请求都被这个拦截器拦截,校验header中的token,token校验通过再放行 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //1.token一般存放在 header 中,所以从 request.getHeader()中获取 token String token = request.getHeader(TOKEN_NAME); Map<String,Object> map = new HashMap<String, Object>(); try { JWTutil.verify(token); map.put("state",true); return true; } catch (AlgorithmMismatchException e) { map.put("msg","JWT算法不匹配!"); } catch (SignatureVerificationException e) { map.put("msg","JWT签名不匹配!"); } catch (TokenExpiredException e) { map.put("msg","token(用户信息)已经过期,请重新登录!"); } catch (Exception e) { map.put("msg","token无效!"); } map.put("state",false); response.setContentType("application/json"); String msg = new ObjectMapper().writeValueAsString(map); response.getWriter().println(msg); return false; } }
package com.jmu.shiro_demo.config; import com.jmu.shiro_demo.intercepetor.JWTIntercepetor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class IntercepetorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new JWTIntercepetor()) .addPathPatterns("/**") //拦截所有请求 .excludePathPatterns("/logout","/js/**","/index","/getAuthCode","/login","/toLogin","/user/**"); // 放行 登陆请求 下面的所有请求 } }
package com.jmu.shiro_demo.service.impl; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.jmu.shiro_demo.entity.Permission; import com.jmu.shiro_demo.entity.Role; import com.jmu.shiro_demo.entity.User; import com.jmu.shiro_demo.mapper.UserMapper; import com.jmu.shiro_demo.service.RoleService; import com.jmu.shiro_demo.service.UserService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.jmu.shiro_demo.utils.JWTutil; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.Subject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.ui.Model; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; /** * <p> * 服务实现类 * </p> * * @author ${author} * @since 2021-04-20 */ @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { @Autowired private RoleService roleService; @Override public String login(String username, String password, String code, HttpSession session, HttpServletResponse response, Model model) { //在校验登陆之前先检验验证码的正确性 String realAuthCode = (String) session.getAttribute("code"); if(!realAuthCode.equalsIgnoreCase(code)) { model.addAttribute("msg","验证码错误!"); return "login"; } //1.获取 Subject Subject subject = SecurityUtils.getSubject(); //2.封装 token UsernamePasswordToken shiroToken = new UsernamePasswordToken(username, password); //3.调用 subject.login(token) 方法 try { subject.login(shiroToken); //登录成功之后返回 token 给客户端 Map<String,String> map = new HashMap<String, String>(); map.put("username",username); String token = JWTutil.getToken(map); //登陆成功,生成JWT token response.setHeader("token",token); // 将 token 设置在 header 里面 model.addAttribute("token",token); }catch (UnknownAccountException e1) { //用户名不存在 model.addAttribute("msg","用户名不存在!"); return "/login"; }catch (IncorrectCredentialsException e2) { model.addAttribute("msg","密码错误!"); return "/login"; } return "/index"; } @Override public User getUserByUserName(String username) { QueryWrapper<User> wrapper = new QueryWrapper<User>(); wrapper.eq("username",username); return this.baseMapper.selectOne(wrapper); } @Override public List<Permission> getUserPermissionsByUserId(Integer userId) { List<Role> roles = getUserRoleByUserId(userId); List<Permission> permissions = new LinkedList<Permission>(); roles.stream().forEach(role -> { permissions.addAll(roleService.getRolePermissionsByRoleId(role.getRoleid())); }); return permissions; } @Override public List<Role> getUserRoleByUserId(Integer userId) { return this.baseMapper.getUserRoleByUserId(userId); } @Override public String logout() { SecurityUtils.getSubject().logout(); return "redirect:/index"; } }
这是 前后端分离的登陆界面, 登陆成功后在 success 里面进行保存 token 和跳转页面
<!DOCTYPE html> <html lang="en" xmlns:th="https://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Login</title> <script th:src="@{/js/jquery-3.6.0.slim.min.js}"></script> </head> <body> <h3 style="color: red" th:text="${msg}"/> <div> 账号: <input type="text" name="username" id="username"> <br/> 密码: <input type="password" name="password" id="password"> <br/> 验证码: <input type="text" name="code" id="code"><img id="changeImg" src="/getAuthCode" alt=""><br/> <button id="loginBtn">登陆</button> </div> <script> $(function () { //点击更换验证码 拼接随机数 $("#changeImg").click(function(){ $("#changeImg").attr('src',"/getAuthCode?d="+Math.random()); }); $('#loginBtn').click(function () { let username = $('#username').val() let password = $('#password').val() let code = $('#code').val() $.ajax({ url:"/user/login", data:{"username":username,"password":password,"code":code}, method:"POST", success: function (data) { window.localStorage.setItem("token",data.token) //将token设置在页面的 localStorage 中 window.location = data.url //跳转页面 console.log(data) }, error: function () { alert("error!") } }) }) }) </script> </body> </html>
shiro_demo
这里主要演示前后端分离下JWT的效果,shiro 和 redis 的效果在上篇实战博客中有发
演示说明,登陆的时候会保存一个 jwt token 到 客户端(浏览器)的localstorage 中,然后每次发起一个后台请求的时候,在 请求头(header)中就会携带这个 token, 后台如果校验这个 token 通过,就响应请求,在这里我的响应请求的方式,就是简单的成功跳转页面。
小彩蛋: 写这篇博客文章的时候,博主是一边听赵雷(一个低调优秀的民谣歌手)的《鼓楼》这首民谣,真的好听!推荐大家也去听(不是广告