2023-03-11
原文作者:柒's Blog 原文地址:https://blog.52itstyle.vip/archives/1304/

202303111618074701.png

前言

阅读本文需要一定的前后端开发基础,前后端分离已成为互联网项目开发的业界标准使用方式,通过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

阅读全文