spring jwt springboot RESTful API認證方式


RESTful API認證方式

一般來講,對於RESTful API都會有認證(Authentication)和授權(Authorization)過程,保證API的安全性。

Authentication vs. Authorization

Authentication指的是確定這個用戶的身份,Authorization是確定該用戶擁有什么操作權限。

認證方式一般有三種

Basic Authentication

這種方式是直接將用戶名和密碼放到Header中,使用 Authorization: Basic Zm9vOmJhcg== ,使用最簡單但是最不安全。

TOKEN認證

這種方式也是再HTTP頭中,使用 Authorization: Bearer <token> ,使用最廣泛的TOKEN是JWT,通過簽名過的TOKEN。

OAuth2.0

這種方式安全等級最高,但是也是最復雜的。如果不是大型API平台或者需要給第三方APP使用的,沒必要整這么復雜。

一般項目中的RESTful API使用JWT來做認證就足夠了。

什么是JWT

Json web token (JWT), 是為了在網絡應用環境間傳遞聲明而執行的一種基於JSON的開放標准((RFC 7519).該token被設計為緊湊且安全的,特別適用於分布式站點的單點登錄(SSO)場景。JWT的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源,也可以增加一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用於認證,也可被加密。

JWT官網: https://jwt.io/

JWT是由三段信息構成的,將這三段信息文本用.鏈接一起就構成了Jwt字符串。就像這樣:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

 

JWT的構成

第一部分我們稱它為頭部(header),第二部分我們稱其為載荷(payload, 類似於飛機上承載的物品),第三部分是簽證(signature)。

header

jwt的頭部承載兩部分信息:

  • 聲明類型,這里是jwt
  • 聲明加密的算法 通常直接使用 HMAC SHA256

這里的加密算法是單向函數散列算法,常見的有MD5、SHA、HAMC。這里使用基於密鑰的Hash算法HMAC生成散列值。

  • MD5 message-digest algorithm 5 (信息-摘要算法)縮寫,廣泛用於加密和解密技術,常用於文件校驗。校驗?不管文件多大,經過MD5后都能生成唯一的MD5值
  • SHA (Secure Hash Algorithm,安全散列算法),數字簽名等密碼學應用中重要的工具,安全性高於MD5
  • HMAC (Hash Message Authentication Code,散列消息鑒別碼,基於密鑰的Hash算法的認證協議。用公開函數和密鑰產生一個固定長度的值作為認證標識,用這個標識鑒別消息的完整性。常用於接口簽名驗證

完整的頭部就像下面這樣的JSON:

{
&apos;typ&apos;: &apos;JWT&apos;,
&apos;alg&apos;: &apos;HS256&apos;
}

 

然后將頭部進行base64加密(該加密是可以對稱解密的),構成了第一部分

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

 

playload

載荷就是存放有效信息的地方,這些有效信息包含三個部分:

  • 標准中注冊的聲明
  • 公共的聲明
  • 私有的聲明

標准中注冊的聲明 (建議但不強制使用) :

  • iss: jwt簽發者
  • sub: jwt所面向的用戶
  • aud: 接收jwt的一方
  • exp: jwt的過期時間,這個過期時間必須要大於簽發時間
  • nbf: 定義在什么時間之前,該jwt都是不可用的.
  • iat: jwt的簽發時間
  • jti: jwt的唯一身份標識,主要用來作為一次性token,從而回避重放攻擊。

公共的聲明:

公共的聲明可以添加任何的信息,一般添加用戶的相關信息或其他業務需要的必要信息.但不建議添加敏感信息,因為該部分在客戶端可解密

私有的聲明:

私有聲明是提供者和消費者所共同定義的聲明,一般不建議存放敏感信息,因為base64是對稱解密的,意味着該部分信息可以歸類為明文信息。

定義一個payload:

{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}

 

然后將其進行base64加密,得到Jwt的第二部分:

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

 

signature

jwt的第三部分是一個簽證信息,這個簽證信息由三部分組成:

header (base64后的)
payload (base64后的)
secret

這個部分需要base64加密后的header和base64加密后的payload使用.連接組成的字符串,然后通過header中聲明的加密方式進行加鹽secret組合加密,然后就構成了jwt的第三部分。

// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

將這三部分用.連接成一個完整的字符串,構成了最終的jwt:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

 

注意:secret是保存在服務器端的,jwt的簽發生成也是在服務器端的,secret就是用來進行jwt的簽發和jwt的驗證,所以,它就是你服務端的私鑰,在任何場景都不應該流露出去。一旦客戶端得知這個secret, 那就意味着客戶端是可以自我簽發jwt了。

如何應用

一般是在請求頭里加入Authorization,並加上Bearer標注:

fetch('api/user/1', {
headers: {
'Authorization': 'Bearer ' + token
}
})

 

服務器負責解析這個HTTP頭來做用戶認證和授權處理。大致流程如下:

安全相關

JWT協議本身不具備安全傳輸功能,所以必須借助於SSL/TLS的安全通道,所以建議如下:

  1. 不應該在jwt的payload部分存放敏感信息,因為該部分是客戶端可解密的部分。
  2. 保護好secret私鑰,該私鑰非常重要。
  3. 如果可以,請使用https協議

和SpringBoot集成

簡要的說明下我們為什么要用JWT,因為我們要實現完全的前后端分離,所以不可能使用session,cookie的方式進行鑒權,所以JWT就被派上了用場,可以通過一個加密密鑰來進行前后端的鑒權。

程序邏輯:

  1. 我們POST用戶名與密碼到/login進行登入,如果成功返回一個加密token,失敗的話直接返回401錯誤。
  2. 之后用戶訪問每一個需要權限的網址請求必須在header中添加Authorization字段,例如Authorization: token,token為密鑰。
  3. 后台會進行token的校驗,如果不通過直接返回401。

這里我講一下如何在SpringBoot中使用JWT來做接口權限認證,安全框架依舊使用Shiro,JWT的實現使用 jjwt

添加Maven依賴

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.3.0</version>
</dependency>
<!-- shiro 權限控制 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
<exclusions>
<exclusion>
<artifactId>slf4j-api</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- shiro ehcache (shiro緩存)-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.0</version>
<exclusions>
<exclusion>
<artifactId>slf4j-api</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>

創建用戶Service

這個在shiro一節講過如果創建角色權限表,添加用戶Service來執行查找用戶操作,這里就不多講具體實現了,只列出關鍵代碼:

/**
* 通過名稱查找用戶
*
* @param username
* @return
*/
public ManagerInfo findByUsername(String username) {
ManagerInfo managerInfo = managerInfoDao.findByUsername(username);
if (managerInfo == null) {
throw new UnknownAccountException();
}
return managerInfo;
}

用戶信息類:

public class ManagerInfo implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主鍵ID
*/
private Integer id;
/**
* 賬號
*/
private String username;
/**
* 密碼
*/
private String password;
/**
* md5密碼鹽
*/
private String salt;
/**
* 一個管理員具有多個角色
*/
private List<SysRole> roles;

 

JWT工具類

我們寫一個簡單的JWT加密,校驗工具,並且使用用戶自己的密碼充當加密密鑰,這樣保證了token 即使被他人截獲也無法破解。並且我們在token中附帶了username信息,並且設置密鑰5分鍾就會過期。

public class JWTUtil {

// 過期時間5分鍾
private static final long EXPIRE_TIME = 5 * 60 * 1000;

/**
* 校驗token是否正確
*
* @param token 密鑰
* @param secret 用戶的密碼
* @return 是否正確
*/
public static boolean verify(String token, String username, String secret) {
try {
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("username", username)
.build();
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (Exception exception) {
return false;
}
}

/**
* 獲得token中的信息無需secret解密也能獲得
*
* @return token中包含的用戶名
*/
public static String getUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
return null;
}
}

/**
* 生成簽名,5min后過期
*
* @param username 用戶名
* @param secret 用戶的密碼
* @return 加密的token
*/
public static String sign(String username, String secret) {
try {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(secret);
// 附帶username信息
return JWT.create()
.withClaim("username", username)
.withExpiresAt(date)
.sign(algorithm);
} catch (UnsupportedEncodingException e) {
return null;
}
}
}

編寫登錄接口

為了讓用戶登錄的時候獲取到正確的JWT Token,需要實現登錄接口,這里我編寫一個 LoginController.java 

@RestController
public class LoginController {

@Resource
private ManagerInfoService managerInfoService;

private static final Logger _logger = LoggerFactory.getLogger(LoginController.class);

@PostMapping("/login")
public BaseResponse login(@RequestParam("username") String username,
@RequestParam("password") String password) {
ManagerInfo user = managerInfoService.findByUsername(username);
//鹽(用戶名+隨機數)
String salt = user.getSalt();
//原密碼
String encodedPassword = ShiroKit.md5(password, username + salt);
if (user.getPassword().equals(encodedPassword)) {
return new BaseResponse(true, "Login success", JWTUtil.sign(username, encodedPassword));
} else {
throw new UnauthorizedException();
}
}

@RequestMapping(path = "/401")
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public BaseResponse unauthorized() {
return new BaseResponse(false, "Unauthorized", null);
}

}

注意上面登錄的時候,我會從數據庫中把這個用戶取出來,密碼加鹽算MD5值比較,通過之后再用密碼作為密鑰來簽名生成JWT。

編寫RESTful接口

先編寫一個通用的接口返回類:

/**
* API接口的基礎返回類
*
* @author XiongNeng
* @version 1.0
* @since 2018/1/7
*/
public class BaseResponse<T> {
/**
* 是否成功
*/
private boolean success;

/**
* 說明
*/
private String msg;

/**
* 返回數據
*/
private T data;

public BaseResponse() {

}

public BaseResponse(boolean success, String msg, T data) {
this.success = success;
this.msg = msg;
this.data = data;
}
}

通過SpringMVC實現RESTful接口,這里我只寫一個示例方法:


@RestController
@RequestMapping(value = "/api/v1")
public class PublicController {

@Resource
private ApiService apiService;
/**
* 請求入網接口
*
* @return 處理結果
*/
@RequestMapping(value = "/join", method = RequestMethod.POST)
@RequiresRoles("admin")
public ResponseEntity<BaseResponse> doJoin(@RequestBody PosParam posParam) {
_logger.info("請求入網接口 start....");
BaseResponse result = new BaseResponse();
// imei碼約束檢查
if (StringUtils.isEmpty(posParam.getImei()) || posParam.getImei().length() > 32) {
result.setSuccess(false);
result.setMsg("IMEI碼長度不是1-32位,入網失敗。");
return new ResponseEntity<>(result, HttpStatus.OK);
}
Pos pos = new Pos();
Date now = new Date();
pos.setJointime(now);
pos.setBindtime(now);
BeanUtils.copyProperties(posParam, pos);
// 插入一條新紀錄
pos.setProjectId(project.getId());
int insert = apiService.insertPos(pos);
if (insert > 0) {
result.setSuccess(true);
result.setMsg("入網成功");
return new ResponseEntity<>(result, HttpStatus.CREATED);
} else {
result.setSuccess(false);
result.setMsg("入網失敗,請聯系管理員。");
return new ResponseEntity<>(result, HttpStatus.OK);
}
}
}

自定義異常

為了實現我自己能夠手動拋出異常,我自己寫了一個 UnauthorizedException.java

public class UnauthorizedException extends RuntimeException {
public UnauthorizedException(String msg) {
super(msg);
}

public UnauthorizedException() {
super();
}
}

處理框架異常

之前說過restful要統一返回的格式,所以我們也要全局處理Spring Boot的拋出異常。利用@RestControllerAdvice能很好的實現。注意這個統一異常處理器只對認證過的用戶調用接口中的異常有作用,對AuthenticationException沒有用

@RestControllerAdvice
public class ExceptionController {

// 捕捉shiro的異常
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ExceptionHandler(ShiroException.class)
public BaseResponse handle401(ShiroException e) {
return new BaseResponse(false, "shiro的異常", null);
}

// 捕捉UnauthorizedException
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ExceptionHandler(UnauthorizedException.class)
public BaseResponse handle401() {
return new BaseResponse(false, "UnauthorizedException", null);
}

// 捕捉其他所有異常
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public BaseResponse globalException(HttpServletRequest request, Throwable ex) {
return new BaseResponse(false, "其他異常", null);
}

private HttpStatus getStatus(HttpServletRequest request) {
Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
if (statusCode == null) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
return HttpStatus.valueOf(statusCode);
}
}

配置Shiro

大家可以先看下官方的 Spring-Shiro 整合教程,有個初步的了解。不過既然我們用了SpringBoot,那我們肯定要爭取零配置文件。

實現JWTToken

JWTToken差不多就是Shiro用戶名密碼的載體。因為我們是前后端分離,服務器無需保存用戶狀態,所以不需要RememberMe這類功能,我們簡單的實現下AuthenticationToken接口即可。因為token自己已經包含了用戶名等信息,所以這里我就弄了一個字段。如果你喜歡鑽研,可以看看官方的UsernamePasswordToken是如何實現的。

public class JWTToken implements AuthenticationToken {

// 密鑰
private String token;

public JWTToken(String token) {
this.token = token;
}

@Override
public Object getPrincipal() {
return token;
}

@Override
public Object getCredentials() {
return token;
}
}

實現Realm

realm的用於處理用戶是否合法的這一塊,需要我們自己實現。

/**
* Description : 身份校驗核心類
*/

public class MyShiroRealm extends AuthorizingRealm {

private static final Logger _logger = LoggerFactory.getLogger(MyShiroRealm.class);

@Autowired
ManagerInfoService managerInfoService;

/**
* JWT簽名密鑰,這里沒用。我使用的是用戶的MD5密碼作為簽名密鑰
*/
public static final String SECRET = "9281e268b77b7c439a20b46fd1483b9a";

/**
* 必須重寫此方法,不然Shiro會報錯
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}

/**
* 認證信息(身份驗證)
* Authentication 是用來驗證用戶身份
*
* @param auth
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth)
throws AuthenticationException {
_logger.info("MyShiroRealm.doGetAuthenticationInfo()");

String token = (String) auth.getCredentials();
// 解密獲得username,用於和數據庫進行對比
String username = JWTUtil.getUsername(token);
if (username == null) {
throw new AuthenticationException("token invalid");
}

//通過username從數據庫中查找 ManagerInfo對象
//實際項目中,這里可以根據實際情況做緩存,如果不做,Shiro自己也是有時間間隔機制,2分鍾內不會重復執行該方法
ManagerInfo managerInfo = managerInfoService.findByUsername(username);

if (managerInfo == null) {
throw new AuthenticationException("User didn't existed!");
}

if (!JWTUtil.verify(token, username, managerInfo.getPassword())) {
throw new AuthenticationException("Username or password error");
}

return new SimpleAuthenticationInfo(token, token, "my_realm");
}

/**
* 此方法調用hasRole,hasPermission的時候才會進行回調.
* <p>
* 權限信息.(授權):
* 1、如果用戶正常退出,緩存自動清空;
* 2、如果用戶非正常退出,緩存自動清空;
* 3、如果我們修改了用戶的權限,而用戶不退出系統,修改的權限無法立即生效。
* (需要手動編程進行實現;放在service進行調用)
* 在權限修改后調用realm中的方法,realm已經由spring管理,所以從spring中獲取realm實例,調用clearCached方法;
* :Authorization 是授權訪問控制,用於對用戶進行的操作授權,證明該用戶是否允許進行當前操作,如訪問某個鏈接,某個資源文件等。
*
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
/*
* 當沒有使用緩存的時候,不斷刷新頁面的話,這個代碼會不斷執行,
* 當其實沒有必要每次都重新設置權限信息,所以我們需要放到緩存中進行管理;
* 當放到緩存中時,這樣的話,doGetAuthorizationInfo就只會執行一次了,
* 緩存過期之后會再次執行。
*/
_logger.info("權限配置-->MyShiroRealm.doGetAuthorizationInfo()");
String username = JWTUtil.getUsername(principals.toString());

// 下面的可以使用緩存提升速度
ManagerInfo managerInfo = managerInfoService.findByUsername(username);

SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();

//設置相應角色的權限信息
for (SysRole role : managerInfo.getRoles()) {
//設置角色
authorizationInfo.addRole(role.getRole());
for (Permission p : role.getPermissions()) {
//設置權限
authorizationInfo.addStringPermission(p.getPermission());
}
}
return authorizationInfo;
}

}

在 doGetAuthenticationInfo 中用戶可以自定義拋出很多異常,詳情見文檔。

重寫Filter

所有的請求都會先經過Filter,所以我們繼承官方的 BasicHttpAuthenticationFilter ,並且重寫鑒權的方法,另外通過重寫preHandle,實現跨越訪問。

代碼的執行流程 preHandle->isAccessAllowed->isLoginAttempt->executeLogin

public class JWTFilter extends BasicHttpAuthenticationFilter {

private Logger LOGGER = LoggerFactory.getLogger(this.getClass());

/**
* 判斷用戶是否想要登入。
* 檢測header里面是否包含Authorization字段即可
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String authorization = req.getHeader("Authorization");
return authorization != null;
}

/**
*
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String authorization = httpServletRequest.getHeader("Authorization");
JWTToken token = new JWTToken(authorization);
// 提交給realm進行登入,如果錯誤他會拋出異常並被捕獲
getSubject(request, response).login(token);
// 如果沒有拋出異常則代表登入成功,返回true
return true;
}

/**
* 這里我們詳細說明下為什么最終返回的都是true,即允許訪問
* 例如我們提供一個地址 GET /article
* 登入用戶和游客看到的內容是不同的
* 如果在這里返回了false,請求會被直接攔截,用戶看不到任何東西
* 所以我們在這里返回true,Controller中可以通過 subject.isAuthenticated() 來判斷用戶是否登入
* 如果有些資源只有登入用戶才能訪問,我們只需要在方法上面加上 @RequiresAuthentication 注解即可
* 但是這樣做有一個缺點,就是不能夠對GET,POST等請求進行分別過濾鑒權(因為我們重寫了官方的方法),但實際上對應用影響不大
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if (isLoginAttempt(request, response)) {
try {
executeLogin(request, response);
} catch (Exception e) {
response401(request, response);
}
}
return true;
}

/**
* 對跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域時會首先發送一個option請求,這里我們給option請求直接返回正常狀態
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}

/**
* 將非法請求跳轉到 /401
*/
private void response401(ServletRequest req, ServletResponse resp) {
try {
HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
httpServletResponse.sendRedirect("/401");
} catch (IOException e) {
LOGGER.error(e.getMessage());
}
}
}

編寫ShiroConfig配置類

這里我還增加了EhCache緩存管理支持,不需要每次都調用數據庫做授權。

@Configuration
@Order(1)
public class ShiroConfig {
/**
* ShiroFilterFactoryBean 處理攔截資源文件問題。
* 注意:單獨一個ShiroFilterFactoryBean配置是或報錯的,以為在
* 初始化ShiroFilterFactoryBean的時候需要注入:SecurityManager Filter Chain定義說明
* 1、一個URL可以配置多個Filter,使用逗號分隔
* 2、當設置多個過濾器時,全部驗證通過,才視為通過
* 3、部分過濾器可指定參數,如perms,roles
*/
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {

ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必須設置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
//驗證碼過濾器
Map<String, Filter> filtersMap = shiroFilterFactoryBean.getFilters();
filtersMap.put("jwt", new JWTFilter());
shiroFilterFactoryBean.setFilters(filtersMap);

// 攔截器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();

// 其他的
filterChainDefinitionMap.put("/**", "jwt");

// 訪問401和404頁面不通過我們的Filter
filterChainDefinitionMap.put("/401", "anon");
filterChainDefinitionMap.put("/404", "anon");

shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}

@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 設置realm.
securityManager.setRealm(myShiroRealm());
//注入緩存管理器
securityManager.setCacheManager(ehCacheManager());
// 關閉shiro自帶的session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);

return securityManager;
}

/**
* 身份認證realm; (這個需要自己寫,賬號密碼校驗;權限等)
*/
@Bean
public MyShiroRealm myShiroRealm() {
MyShiroRealm myShiroRealm = new MyShiroRealm();
return myShiroRealm;
}

/**
* 開啟shiro aop注解支持. 使用代理方式; 所以需要開啟代碼支持;
*
* @param securityManager 安全管理器
* @return 授權Advisor
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}

/**
* shiro緩存管理器;
* 需要注入對應的其它的實體類中:
* 1、安全管理器:securityManager
* 可見securityManager是整個shiro的核心;
*
* @return
*/
@Bean
public EhCacheManager ehCacheManager() {
EhCacheManager cacheManager = new EhCacheManager();
cacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
return cacheManager;
}

}

里面URL規則自己參考文檔 http://shiro.apache.org/web.html ,這個在shiro那篇說的很清楚了。

運行驗證

最后是將代碼跑起來驗證這一切是否正常。

啟動SpringBoot后,先通過POST請求登錄拿到token

然后在調用入網接口的時候在header中帶上這個token認證:

如果token認證不正確會報異常:

如果使用普通用戶登錄,認證正確但是授權訪問接口失敗,會返回如下的未授權結果:

參考文章

RESTful API認證方式

一般來講,對於RESTful API都會有認證(Authentication)和授權(Authorization)過程,保證API的安全性。

Authentication vs. Authorization

Authentication指的是確定這個用戶的身份,Authorization是確定該用戶擁有什么操作權限。

認證方式一般有三種

Basic Authentication

這種方式是直接將用戶名和密碼放到Header中,使用 Authorization: Basic Zm9vOmJhcg== ,使用最簡單但是最不安全。

TOKEN認證

這種方式也是再HTTP頭中,使用 Authorization: Bearer <token> ,使用最廣泛的TOKEN是JWT,通過簽名過的TOKEN。

OAuth2.0

這種方式安全等級最高,但是也是最復雜的。如果不是大型API平台或者需要給第三方APP使用的,沒必要整這么復雜。

一般項目中的RESTful API使用JWT來做認證就足夠了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM