前言
閱讀本文需要一定的前後端開發基礎,前後端分離已成為網際網路專案開發的業界標準使用方式,通過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
簽發了,在到期之前就會始終有效,除非伺服器部署額外的邏輯。