阅读本文需要一定的前后端开发基础,前后端分离已成为互联网项目开发的业界标准使用方式,通过Nginx代理+Tomcat的方式有效的进行解耦,并且前后端分离会为以后的大型分布式架构、弹性计算架构、微服务架构、多端化服务(多种客户端,例如:浏览器,小程序,安卓,IOS等等)打下坚实的基础。这个步骤是系统架构从猿进化成人的必经之路。

其核心思想是前端页面通过AJAX调用后端的API接口并使用JSON数据进行交互。

开发者通常使用Servlet、Jsp、Velocity、Freemaker、Thymeleaf以及各种框架模板标签的方式实现前端效果展示。通病就是,后端开发者从后端撸到前端,前端只负责切切页面,修修图,更有甚者,一些团队都没有所谓的前端。

在传统架构模式中,前后端代码存放于同一个代码库中,甚至是同一工程目录下。页面中还夹杂着后端代码。前后端分离以后,前后端分成了两个不同的代码库,通常使用 Vue、React、Angular、Layui等一系列前端框架实现。

回到文章的主题,这里我们使用目前最流行的跨域认证解决方案JSON Web Token(缩写 JWT

pom.xml引入:

  1. <dependency>
  2. <groupId>io.jsonwebtoken</groupId>
  3. <artifactId>jjwt</artifactId>
  4. <version>0.9.1</version>
  5. </dependency>

工具类,签发JWT,可以存储简单的用户基础信息,比如用户ID、用户名等等,只要能识别用户信息即可,重要的角色权限不建议存储:

  1. /**
  2. * JWT加密和解密的工具类
  3. */
  4. public class JwtUtils {
  5. /**
  6. * 加密字符串 禁泄漏
  7. */
  8. public static final String SECRET = "e3f4e0ffc5e04432a63730a65f0792b0";
  9. public static final int JWT_ERROR_CODE_NULL = 4000; // Token不存在
  10. public static final int JWT_ERROR_CODE_EXPIRE = 4001; // Token过期
  11. public static final int JWT_ERROR_CODE_FAIL = 4002; // 验证不通过
  12. /**
  13. * 签发JWT
  14. * @param id
  15. * @param subject
  16. * @param ttlMillis
  17. * @return String
  18. */
  19. public static String createJWT(String id, String subject, long ttlMillis) {
  20. SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
  21. long nowMillis = System.currentTimeMillis();
  22. Date now = new Date(nowMillis);
  23. SecretKey secretKey = generalKey();
  24. JwtBuilder builder = Jwts.builder()
  25. .setId(id)
  26. .setSubject(subject) // 主题
  27. .setIssuer("爪哇笔记") // 签发者
  28. .setIssuedAt(now) // 签发时间
  29. .signWith(signatureAlgorithm, secretKey); // 签名算法以及密匙
  30. if (ttlMillis >= 0) {
  31. long expMillis = nowMillis + ttlMillis;
  32. Date expDate = new Date(expMillis);
  33. builder.setExpiration(expDate); // 过期时间
  34. }
  35. return builder.compact();
  36. }
  37. /**
  38. * 验证JWT
  39. * @param jwtStr
  40. * @return CheckResult
  41. */
  42. public static CheckResult validateJWT(String jwtStr) {
  43. CheckResult checkResult = new CheckResult();
  44. Claims claims;
  45. try {
  46. claims = parseJWT(jwtStr);
  47. checkResult.setSuccess(true);
  48. checkResult.setClaims(claims);
  49. } catch (ExpiredJwtException e) {
  50. checkResult.setErrCode(JWT_ERROR_CODE_EXPIRE);
  51. checkResult.setSuccess(false);
  52. } catch (SignatureException e) {
  53. checkResult.setErrCode(JWT_ERROR_CODE_FAIL);
  54. checkResult.setSuccess(false);
  55. } catch (Exception e) {
  56. checkResult.setErrCode(JWT_ERROR_CODE_FAIL);
  57. checkResult.setSuccess(false);
  58. }
  59. return checkResult;
  60. }
  61. /**
  62. * 密钥
  63. * @return
  64. */
  65. public static SecretKey generalKey() {
  66. byte[] encodedKey = Base64.decode(SECRET);
  67. SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
  68. return key;
  69. }
  70. /**
  71. * 解析JWT字符串
  72. * @param jwt
  73. * @return
  74. * @throws Exception Claims
  75. */
  76. public static Claims parseJWT(String jwt) {
  77. SecretKey secretKey = generalKey();
  78. return Jwts.parser()
  79. .setSigningKey(secretKey)
  80. .parseClaimsJws(jwt)
  81. .getBody();
  82. }
  83. }

验证实体信息:

  1. /**
  2. * 验证信息
  3. */
  4. public class CheckResult {
  5. private int errCode;
  6. private boolean success;
  7. private Claims claims;
  8. public int getErrCode() {
  9. return errCode;
  10. }
  11. public void setErrCode(int errCode) {
  12. this.errCode = errCode;
  13. }
  14. public boolean isSuccess() {
  15. return success;
  16. }
  17. public void setSuccess(boolean success) {
  18. this.success = success;
  19. }
  20. public Claims getClaims() {
  21. return claims;
  22. }
  23. public void setClaims(Claims claims) {
  24. this.claims = claims;
  25. }
  26. }

拦截访问配置,跨域访问设置以及请求拦截过滤:

  1. /**
  2. * 拦截访问配置
  3. */
  4. @Configuration
  5. public class SafeConfig implements WebMvcConfigurer {
  6. @Bean
  7. public SysInterceptor myInterceptor(){
  8. return new SysInterceptor();
  9. }
  10. @Override
  11. public void addCorsMappings(CorsRegistry registry) {
  12. registry.addMapping("/**")
  13. .allowedOrigins("*")
  14. .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE","OPTIONS")
  15. .allowCredentials(false).maxAge(3600);
  16. }
  17. @Override
  18. public void addInterceptors(InterceptorRegistry registry) {
  19. String[] patterns = new String[] { "/user/login","/*.html"};
  20. registry.addInterceptor(myInterceptor())
  21. .addPathPatterns("/**")
  22. .excludePathPatterns(patterns);
  23. }
  24. }

拦截器统一权限校验:

  1. /**
  2. * 认证拦截器
  3. */
  4. public class SysInterceptor implements HandlerInterceptor {
  5. private static final Logger logger = LoggerFactory.getLogger(SysInterceptor.class);
  6. @Autowired
  7. private SysUserService sysUserService;
  8. @Override
  9. public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
  10. Object handler){
  11. if (handler instanceof HandlerMethod){
  12. String authHeader = request.getHeader("token");
  13. if (StringUtils.isEmpty(authHeader)) {
  14. logger.info("验证失败");
  15. print(response,Result.error(JwtUtils.JWT_ERROR_CODE_NULL,"签名验证不存在,请重新登录"));
  16. return false;
  17. }else{
  18. CheckResult checkResult = JwtUtils.validateJWT(authHeader);
  19. if (checkResult.isSuccess()) {
  20. /**
  21. * 权限验证
  22. */
  23. String userId = checkResult.getClaims().getId();
  24. HandlerMethod handlerMethod = (HandlerMethod) handler;
  25. Annotation roleAnnotation= handlerMethod.getMethod().getAnnotation(RequiresRoles.class);
  26. if(roleAnnotation!=null){
  27. String[] role = handlerMethod.getMethod().getAnnotation(RequiresRoles.class).value();
  28. Logical logical = handlerMethod.getMethod().getAnnotation(RequiresRoles.class).logical();
  29. List<String> list = sysUserService.getRoleSignByUserId(Integer.parseInt(userId));
  30. int count = 0;
  31. for(int i=0;i<role.length;i++){
  32. if(list.contains(role[i])){
  33. count++;
  34. if(logical==Logical.OR){
  35. continue;
  36. }
  37. }
  38. }
  39. if(logical==Logical.OR){
  40. if(count==0){
  41. print(response,Result.error("无权限操作"));
  42. return false;
  43. }
  44. }else{
  45. if(count!=role.length){
  46. print(response,Result.error("无权限操作"));
  47. return false;
  48. }
  49. }
  50. }
  51. return true;
  52. } else {
  53. switch (checkResult.getErrCode()) {
  54. case SystemConstant.JWT_ERROR_CODE_FAIL:
  55. logger.info("签名验证不通过");
  56. print(response,Result.error(checkResult.getErrCode(),"签名验证不通过,请重新登录"));
  57. break;
  58. case SystemConstant.JWT_ERROR_CODE_EXPIRE:
  59. logger.info("签名过期");
  60. print(response,Result.error(checkResult.getErrCode(),"签名过期,请重新登录"));
  61. break;
  62. default:
  63. break;
  64. }
  65. return false;
  66. }
  67. }
  68. }else{
  69. return true;
  70. }
  71. }
  72. /**
  73. * 打印输出
  74. * @param response
  75. * @param message void
  76. */
  77. public void print(HttpServletResponse response,Object message){
  78. try {
  79. response.setStatus(HttpStatus.OK.value());
  80. response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
  81. response.setHeader("Cache-Control", "no-cache, must-revalidate");
  82. response.setHeader("Access-Control-Allow-Origin", "*");
  83. PrintWriter writer = response.getWriter();
  84. writer.write(JSONObject.toJSONString(message));
  85. writer.flush();
  86. writer.close();
  87. } catch (IOException e) {
  88. e.printStackTrace();
  89. }
  90. }
  91. }

配置角色注解,可以直接把安全框架Shiro的拷贝过来,如果有需要,菜单权限也可以配置上:

  1. /**
  2. * 权限注解
  3. */
  4. @Target({ElementType.TYPE, ElementType.METHOD})
  5. @Retention(RetentionPolicy.RUNTIME)
  6. public @interface RequiresRoles {
  7. /**
  8. * A single String role name or multiple comma-delimitted role names required in order for the method
  9. * invocation to be allowed.
  10. */
  11. String[] value();
  12. /**
  13. * The logical operation for the permission check in case multiple roles are specified. AND is the default
  14. * @since 1.1.0
  15. */
  16. Logical logical() default Logical.OR;
  17. }

模拟演示代码:

  1. @RestController
  2. @RequestMapping("/user")
  3. public class UserController {
  4. /**
  5. * 列表
  6. * @return
  7. */
  8. @RequestMapping("/list")
  9. @RequiresRoles(value="admin")
  10. public Result list() {
  11. return Result.ok("十万亿个用户");
  12. }
  13. /**
  14. * 登录
  15. * @return
  16. */
  17. @RequestMapping("/login")
  18. public Result login() {
  19. /**
  20. * 模拟登录过程并返回token
  21. */
  22. String token = JwtUtils.createJWT("101","爪哇笔记",1000*60*60);
  23. return Result.ok(token);
  24. }
  25. }

前端请求模拟,发送请求之前在Header中附带token信息,更多代码见源码案例:

  1. function login(){
  2. $.ajax({
  3. url : "/user/login",
  4. type : "post",
  5. dataType : "json",
  6. success : function(data) {
  7. if(data.code==0){
  8. $.cookie('token', data.msg);
  9. }
  10. },
  11. error : function(XMLHttpRequest, textStatus, errorThrown) {
  12. }
  13. });
  14. }
  15. function user(){
  16. $.ajax({
  17. url : "/user/list",
  18. type : "post",
  19. dataType : "json",
  20. success : function(data) {
  21. alert(data.msg)
  22. },
  23. beforeSend: function(request) {
  24. request.setRequestHeader("token", $.cookie('token'));
  25. },
  26. error : function(XMLHttpRequest, textStatus, errorThrown) {
  27. }
  28. });
  29. }
  30. </script>

JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期建议设置的相对短一些。对于一些比较重要的权限,使用时应该再次对用户进行数据库认证。为了减少盗用,JWT 强烈建议使用 HTTPS 协议传输。

由于服务器不保存用户状态,因此无法在使用过程中注销某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。

https://gitee.com/52itstyle/safe-jwt

版权声明:本文为smallSevens原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/smallSevens/p/12712744.html