• SpringBoot 2.x 开发案例之前后端分离鉴权


    前言

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

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

    原始模式

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

    分离模式

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

    权限校验

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

    pom.xml引入:

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

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

    /**
     * JWT加密和解密的工具类
     */
    public class JwtUtils {
    	/**
    	 * 加密字符串 禁泄漏
    	 */
    	public static final String SECRET = "e3f4e0ffc5e04432a63730a65f0792b0";
    	public static final int JWT_ERROR_CODE_NULL = 4000; // Token不存在
    	public static final int JWT_ERROR_CODE_EXPIRE = 4001; // Token过期
    	public static final int JWT_ERROR_CODE_FAIL = 4002; // 验证不通过
    
        /**
         * 签发JWT
         * @param id
         * @param subject
         * @param ttlMillis
         * @return  String
         */
    	public static String createJWT(String id, String subject, long ttlMillis) {
    		SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
    		long nowMillis = System.currentTimeMillis();
    		Date now = new Date(nowMillis);
    		SecretKey secretKey = generalKey();
    		JwtBuilder builder = Jwts.builder()
    				.setId(id)
    				.setSubject(subject)   // 主题
    				.setIssuer("爪哇笔记")     // 签发者
    				.setIssuedAt(now)      // 签发时间
    				.signWith(signatureAlgorithm, secretKey); // 签名算法以及密匙
    		if (ttlMillis >= 0) {
    			long expMillis = nowMillis + ttlMillis;
    			Date expDate = new Date(expMillis);
    			builder.setExpiration(expDate); // 过期时间
    		}
    		return builder.compact();
    	}
        /**
         * 验证JWT
         * @param jwtStr
         * @return  CheckResult
         */
    	public static CheckResult validateJWT(String jwtStr) {
    		CheckResult checkResult = new CheckResult();
    		Claims claims;
    		try {
    			claims = parseJWT(jwtStr);
    			checkResult.setSuccess(true);
    			checkResult.setClaims(claims);
    		} catch (ExpiredJwtException e) {
    			checkResult.setErrCode(JWT_ERROR_CODE_EXPIRE);
    			checkResult.setSuccess(false);
    		} catch (SignatureException e) {
    			checkResult.setErrCode(JWT_ERROR_CODE_FAIL);
    			checkResult.setSuccess(false);
    		} catch (Exception e) {
    			checkResult.setErrCode(JWT_ERROR_CODE_FAIL);
    			checkResult.setSuccess(false);
    		}
    		return checkResult;
    	}
    
    	/**
    	 * 密钥
    	 * @return
    	 */
    	public static SecretKey generalKey() {
    		byte[] encodedKey = Base64.decode(SECRET);
    	    SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
    	    return key;
    	}
    	
    	/**
    	 * 解析JWT字符串
    	 * @param jwt
    	 * @return
    	 * @throws Exception  Claims
    	 */
    	public static Claims parseJWT(String jwt) {
    		SecretKey secretKey = generalKey();
    		return Jwts.parser()
    			.setSigningKey(secretKey)
    			.parseClaimsJws(jwt)
    			.getBody();
    	}
    }
    

    验证实体信息:

    /**
     * 验证信息
     */
    public class CheckResult {
    	private int errCode;
    
    	private boolean success;
    
    	private Claims claims;
    
    	public int getErrCode() {
    		return errCode;
    	}
    
    	public void setErrCode(int errCode) {
    		this.errCode = errCode;
    	}
    
    	public boolean isSuccess() {
    		return success;
    	}
    
    	public void setSuccess(boolean success) {
    		this.success = success;
    	}
    
    	public Claims getClaims() {
    		return claims;
    	}
    
    	public void setClaims(Claims claims) {
    		this.claims = claims;
    	}
    }
    

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

    /**
     * 拦截访问配置
     */
    @Configuration
    public class SafeConfig implements WebMvcConfigurer {
    
        @Bean
        public SysInterceptor myInterceptor(){
            return new SysInterceptor();
        }
    
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/**")
                    .allowedOrigins("*")
                    .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE","OPTIONS")
                    .allowCredentials(false).maxAge(3600);
        }
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            String[] patterns = new String[] { "/user/login","/*.html"};
            registry.addInterceptor(myInterceptor())
                    .addPathPatterns("/**")
                    .excludePathPatterns(patterns);
        }
    }
    

    拦截器统一权限校验:

    /**
     * 认证拦截器
     */
    public class SysInterceptor  implements HandlerInterceptor {
    	
    	private static final Logger logger = LoggerFactory.getLogger(SysInterceptor.class);
    	
    	@Autowired
        private SysUserService sysUserService;
    	
    	@Override
    	public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
    			Object handler){
    		if (handler instanceof HandlerMethod){
        		String authHeader = request.getHeader("token");
            	if (StringUtils.isEmpty(authHeader)) {
    				  logger.info("验证失败");
    				  print(response,Result.error(JwtUtils.JWT_ERROR_CODE_NULL,"签名验证不存在,请重新登录"));
    				  return false;
                }else{
                    CheckResult checkResult = JwtUtils.validateJWT(authHeader);
                    if (checkResult.isSuccess()) {
    					/**
    					 * 权限验证
    					 */
    					String userId = checkResult.getClaims().getId();
    					HandlerMethod handlerMethod = (HandlerMethod) handler;
    					Annotation roleAnnotation= handlerMethod.getMethod().getAnnotation(RequiresRoles.class);
    					if(roleAnnotation!=null){
    						String[] role = handlerMethod.getMethod().getAnnotation(RequiresRoles.class).value();
    						Logical logical = handlerMethod.getMethod().getAnnotation(RequiresRoles.class).logical();
    						List<String> list = sysUserService.getRoleSignByUserId(Integer.parseInt(userId));
    						int count = 0;
    						for(int i=0;i<role.length;i++){
    							if(list.contains(role[i])){
    								count++;
    								if(logical==Logical.OR){
    									continue;
    								}
    							}
    						}
    						if(logical==Logical.OR){
    							if(count==0){
    								print(response,Result.error("无权限操作"));
    								return false;
    							}
    						}else{
    							if(count!=role.length){
    								print(response,Result.error("无权限操作"));
    								return false;
    							}
    						}
    					}
                    	return true;
                    } else {
                        switch (checkResult.getErrCode()) {
    	                    case SystemConstant.JWT_ERROR_CODE_FAIL:
    	                    	logger.info("签名验证不通过");
    	                    	print(response,Result.error(checkResult.getErrCode(),"签名验证不通过,请重新登录"));
    	                    	break;
    	                    case SystemConstant.JWT_ERROR_CODE_EXPIRE:
    	                    	logger.info("签名过期");
    	                    	print(response,Result.error(checkResult.getErrCode(),"签名过期,请重新登录"));
    	                    	break;
    	                    default:
    	                        break;
                        }
                        return false;
                    }
                }
    		}else{
    			return true;
    		}
    	}
    	/**
    	 * 打印输出
    	 * @param response
    	 * @param message  void
    	 */
    	public void print(HttpServletResponse response,Object message){
    		try {
    			response.setStatus(HttpStatus.OK.value());
    			response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
    			response.setHeader("Cache-Control", "no-cache, must-revalidate");
    			response.setHeader("Access-Control-Allow-Origin", "*");
    			PrintWriter writer = response.getWriter();
    			writer.write(JSONObject.toJSONString(message));
    			writer.flush();
    			writer.close();
    		} catch (IOException e) {
    			e.printStackTrace();
    		}
    	 }
    }
    

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

    /**
     * 权限注解
     */
    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface RequiresRoles {
    
        /**
         * A single String role name or multiple comma-delimitted role names required in order for the method
         * invocation to be allowed.
         */
        String[] value();
    
        /**
         * The logical operation for the permission check in case multiple roles are specified. AND is the default
         * @since 1.1.0
         */
        Logical logical() default Logical.OR;
    }
    

    模拟演示代码:

    @RestController
    @RequestMapping("/user")
    public class UserController {
        /**
         * 列表
         * @return
         */
        @RequestMapping("/list")
        @RequiresRoles(value="admin")
        public Result list() {
            return Result.ok("十万亿个用户");
        }
    
        /**
         * 登录
         * @return
         */
        @RequestMapping("/login")
        public Result login() {
            /**
             * 模拟登录过程并返回token
             */
            String token = JwtUtils.createJWT("101","爪哇笔记",1000*60*60);
            return Result.ok(token);
        }
    }
    

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

    function login(){
       $.ajax({
    		url : "/user/login",
    		type : "post",
    		dataType : "json",
    		success : function(data) {
                if(data.code==0){
                   $.cookie('token', data.msg);
                }
    		},
    		error : function(XMLHttpRequest, textStatus, errorThrown) {
    
    		}
    	});
    }
    function user(){
       $.ajax({
    		url : "/user/list",
    		type : "post",
    		dataType : "json",
    		success : function(data) {
                alert(data.msg)
    		},
    		beforeSend: function(request) {
                request.setRequestHeader("token", $.cookie('token'));
            },
    		error : function(XMLHttpRequest, textStatus, errorThrown) {
    
    		}
    	});
    }
    </script>
    

    安全说明

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

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

    源码案例

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

  • 相关阅读:
    开源跳板机(堡垒机)系统 Jumpserver安装教程(带图文)
    运维自动化管理服务器 CheungSSH
    代码托管服务平台GitHub
    Java 博客系统 Tale
    基于代码生成器的快速开发平台 JEECG
    中文企业云操作系统 CecOS
    Python基础学习(一)之Python的概述与环境安装
    Python3.x安装教程及环境变量配置
    PHP Primary script unknown 终极解决方法
    正则表达式备忘录-Regular Expressions Cheatsheet中文版
  • 原文地址:https://www.cnblogs.com/smallSevens/p/12712744.html
Copyright © 2020-2023  润新知