牛逼哄哄的前後端分離如何安全的鑑權

點擊▲關注 “爪哇筆記”   給公衆號標星置頂

更多精彩 第一時間直達

前言

閱讀本文需要一定的前後端開發基礎,前後端分離已成爲互聯網項目開發的業界標準使用方式,通過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) {

        }
    });
}

安全說明

JWT 本身包含了認證信息,一旦泄露,任何人都可以獲得該令牌的所有權限。爲了減少盜用,JWT 的有效期建議設置的相對短一些。對於一些比較重要的權限,使用時應該再次對用戶進行數據庫認證。爲了減少盜用,JWT 強烈建議使用 HTTPS 協議傳輸。

由於服務器不保存用戶狀態,因此無法在使用過程中註銷某個 token,或者更改 token 的權限。也就是說,一旦 JWT 簽發了,在到期之前就會始終有效,除非服務器部署額外的邏輯。

源碼案例

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

分享幾款狂拽炫酷屌炸天的大屏監控場景案例

SpringBoot 2.x的前後端分離商城系統

SpringBoot 2.x+Shiro開發的權限管理腳手架

SpringBoot 2.x炫酷吊前後端分離的後臺管理

Spring Cloud 的企業級前後端分離微服務框架

Spring Cloud 前後端分離架構的權限管理系統


▲一個有溫度的公衆號,期待與你一起進步

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章