jwt 介绍就不多说了,下面通过代码演示开发过程中jwt 的使用。
(1)在pom.xml中引入对应的jar
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.7.0</version> </dependency>
(2)引入jwt 工具类:token的生成以及获取对应的token信息
/**
* @author : wl
* @Description :
* @date : 2020/7/3 13:25
*/
public class JwtUtil {
public static final String AUTHORIZATION_SECRET = "wlcoder";
private static final String UID = "uid";
private static final String USERNAME = "username";
private static final String PASSWORD = "password";
private static final String STATUS = "status";
//创建秘钥
public static Key getKeyInstance() {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
byte[] bytes = DatatypeConverter.parseBase64Binary(AUTHORIZATION_SECRET);
return new SecretKeySpec(bytes, signatureAlgorithm.getJcaName());
}
/**
* 生成token的方法
*
* @param user
* @param expire
* @return
*/
public static String generatorToken(SysUser user, int expire) {
return Jwts.builder().claim(UID, user.getId())
.claim(USERNAME, user.getUsername())
.claim(PASSWORD, user.getPassword())
.claim(STATUS, user.getStatus())
.setExpiration(DateTime.now().plusSeconds(expire).toDate())
.signWith(SignatureAlgorithm.HS256, getKeyInstance())
.compact();
}
/**
* 根据token获取token中的信息
*
* @param token
* @return
*/
public static SysUser getTokenInfo(String token) {
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(getKeyInstance()).parseClaimsJws(token);
Claims claims = claimsJws.getBody();
return SysUser.builder().id((Integer) claims.get(UID))
.username((String) claims.get(USERNAME))
.password((String) claims.get(PASSWORD))
.status((Integer) claims.get(STATUS))
.build();
}
}
(3)添加注解 @NeedToken,@SkipToken ,加在方法上灵活处理对应的请求
/** * @author : wl * @Description : 需要token 验证 * @date : 2020/7/3 11:40 */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface NeedToken { boolean required() default true; }
/** * @author : wl * @Description :跳过token 验证 * @date : 2020/7/3 11:39 */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface SkipToken { boolean required() default true; }
(4)添加拦截器,验证前端请求是否需要token
/** * @author : wl * @Description :方法请求拦截 * @date : 2020/7/3 11:47 */ @Slf4j public class AuthenticationInterceptor implements HandlerInterceptor { @Autowired private SysUserService userService; @Autowired private RedisUtil redisUtil; @Override public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws ServletException, IOException { String token = httpServletRequest.getHeader("token"); if (!(object instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod = (HandlerMethod) object; Method method = handlerMethod.getMethod(); //检查有没有跳过token的注解 if (method.isAnnotationPresent(SkipToken.class)) { SkipToken skipToken = method.getAnnotation(SkipToken.class); if (skipToken.required()) { log.info("该请求无须token验证。。。"); return true; } } //检查有没有需要token的注解 if (method.isAnnotationPresent(NeedToken.class)) { NeedToken needToken = method.getAnnotation(NeedToken.class); if (needToken.required()) { log.info("该请求需要token验证。。。"); if (Objects.isNull(token)) { throw new BaseException("无token,请重新登录"); } try { JwtUtil.getTokenInfo(token); } catch (ExpiredJwtException e) { throw new BaseException("token超时"); } // SysUser user = userService.findUser(sysUser.getUsername(), sysUser.getPassword()); // if (Objects.isNull(user)) { // throw new BaseException("用户不存在,请重新登录"); // } if (!Objects.equals(token, redisUtil.get("ms_notify_token"))) { throw new BaseException("token异常,请重新登录"); } } } return true; } @Override public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { } }
(5)添加拦截器异常处理 :出现异常直接跳转到登录页面 ,这里有个坑,前端为ajax请求时候 使用转发或者重定向会失效。因此处理为 判断请求为ajax请求则设置返回一个状态码如:httpServletResponse.setStatus(666); 前端jquery.js中统一判断处理。
/** * @author : wl * @Description : 拦截异常处理 * @date : 2020/7/5 15:30 */ @Slf4j public class MyWebHandlerException implements HandlerExceptionResolver { @SneakyThrows @Override public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) { log.info("请求出现异常:" + e.getMessage()); e.printStackTrace(); // return new ModelAndView("redirect:/login"); ModelAndView modelAndView = new ModelAndView(); String type = httpServletRequest.getHeader("X-Requested-With"); if (Objects.equals(type, "XMLHttpRequest")) { //是ajax请求 httpServletResponse.setStatus(666); httpServletResponse.setContentType("text/javascript; charset=utf-8"); httpServletResponse.getWriter().write(e.getMessage()); return modelAndView; } else { modelAndView.setViewName("/login"); return modelAndView; } } }
jquery.js中添加对应状态码处理:
//自定义异常信息: 跳转到登录页面 jQuery.ajaxSetup({ statusCode:{ 666:function(data){ alert(data.responseText); window.location.href="/login"; } } })
(6) 添加webConfig配置 实现WebMvcConfigurer,引入自定义的token拦截以及异常处理
/** * @author : wl * @Description :web拦截器 * @date : 2020/7/3 11:46 */ @Configuration public class webConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authenticationInterceptor()).addPathPatterns("/**"); } @Bean public AuthenticationInterceptor authenticationInterceptor() { return new AuthenticationInterceptor(); } @Bean public WebMvcConfigurer webMvcConfigurer() { WebMvcConfigurer adapter = new WebMvcConfigurer() { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName("login"); registry.addViewController("/index.html").setViewName("login"); } }; return adapter; } /* * 异常拦截处理 * */ @Override public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) { resolvers.add(new MyWebHandlerException()); } }
到这里基本配置基本差不多了,web应用中怎么处理呢?例如登录功能:
登录后端代码:
/** * @author : wl * @Description : * @date : 2020/7/2 16:46 */ @Controller public class Login { @Autowired private SysUserService sysUserService; @Autowired private RedisUtil redisUtil; @SkipToken @RequestMapping("/login") public String toLogin() { System.out.println("跳转到登录页面"); return "login"; } @SkipToken @RequestMapping("/index") public String toIndex() { System.out.println("跳转到主页面"); return "index"; } @SkipToken @ResponseBody @RequestMapping("/loginIn") public ResultUtil loginIn(String username, String password) { try { SysUser user = sysUserService.findUser(username, password); if (null != user) { user.setPassword(password); String token = JwtUtil.generatorToken(user, 60*60); //token 保存在redis中 redisUtil.set("ms_notify_token", token); return ResultUtil.ok().data("msg", token).message("登录成功"); } else { return ResultUtil.error().data("msg", "error").message("用户不存在"); } } catch (BaseException e) { return ResultUtil.error().data("msg", e.getMessage()).message("登陆失败"); } } }
登录前端代码:这里需要注意的是 配置window.location.href 不生效 需要检查是否设置为form 表单。
前端接受到token存储在localStorage: localStorage.setItem("token",data.data.msg);
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="description" content=""> <meta name="author" content=""> <title>登录</title> <link href="css/bootstrap.min.css" rel="stylesheet"> <link href="css/signin.css" rel="stylesheet"> </head> <body class="text-center"> <div class="form-signin"> <img class="mb-4" th:src="@{img/my.svg}" alt="" width="80" height="80"> <h1 class="h3 mb-3 font-weight-normal">请登录</h1> <label class="sr-only">用户名</label> <input type="text" name="username" id="username" class="form-control" th:placeholder="用户名" required="required" autofocus=""> <p class=""></p> <label class="sr-only">密码</label> <input type="password" name="password" id="password" class="form-control" th:placeholder="密码" required="required" > <button class="btn btn-lg btn-primary btn-block" th:onclick="loginIn()">登录</button> </div> </body> <script type="text/javascript" src="/js/jquery-3.5.1.js"></script> <script type="text/javascript" src="/js/bootstrap.min.js"></script> <script> function loginIn() { debugger; var username = $('#username').val(); var password = $('#password').val(); if(""==username || ""==password){ alert("用户信息不完整,请检查!"); return; } $.ajax({ url: '/loginIn', type: "post", data: {'username': username, 'password': password}, success: function (data) { if (data.success) { //alert(data.message); localStorage.setItem("token",data.data.msg); location.href = "/index"; // window.location.href 不生效 检查是否为form 表单 } else { alert(data.message) } }, error: function () { alert("登录失败") } }); } </script> </html>
若是需要token验证,前端对应的ajax 请求需要加上headers 如:
//禁用,启用 function disable_config(nid, status) { $.ajax({ url: '/notify/updateStatus', data: {'nid': nid, 'status': status}, type: "post", headers: {"token": localStorage.getItem("token")}, success: function (data) { if (data.success) { location.reload(); alert(data.message); } else { alert(data.message + ":" + data.data.msg) } } }); }
后端对应方法上需要添加注解@NeedToken 如:
/** * 禁用 、启用 */ @NeedToken @ResponseBody @SysLogAnnotation("禁用 、启用 配置") @RequestMapping(value = "/updateStatus") public ResultUtil updateStatus(HttpServletRequest request, String nid, int status) { String config_status = (status == 1 ? "启用" : "禁用"); try { notifyConfigService.updateStatus(nid, status); } catch (BaseException e) { return ResultUtil.error().data("msg", e.getMessage()).message(config_status + "配置失败"); } return ResultUtil.ok().data("msg", "success").message(config_status + "配置成功"); }
设计流程基本如上述代码所示,详细代码可以参考 github地址 : https://github.com/wlcoder/ms-notify
该项目主要是实现自动配置定时发送邮件信息,如果觉得不错,可以star一下。有什么意见或者建议,可以指出来,大家相互学习,谢谢!