spring jwt springboot RESTful API認證方式

RESTful API認證方式javascript

通常來說,對於RESTful API都會有認證(Authentication)和受權(Authorization)過程,保證API的安全性。html

Authentication vs. Authorizationjava

Authentication指的是肯定這個用戶的身份,Authorization是肯定該用戶擁有什麼操做權限。git

認證方式通常有三種github

Basic Authenticationweb

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

TOKEN認證spring

這種方式也是再HTTP頭中,使用 Authorization: Bearer <token> ,使用最普遍的TOKEN是JWT,經過簽名過的TOKEN。數據庫

OAuth2.0apache

這種方式安全等級最高,可是也是最複雜的。若是不是大型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

這裏的加密算法是單向函數散列算法,常見的有MD五、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>
* 權限信息.(受權):
* 一、若是用戶正常退出,緩存自動清空;
* 二、若是用戶非正常退出,緩存自動清空;
* 三、若是咱們修改了用戶的權限,而用戶不退出系統,修改的權限沒法當即生效。
* (須要手動編程進行實現;放在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定義說明
* 一、一個URL能夠配置多個Filter,使用逗號分隔
* 二、當設置多個過濾器時,所有驗證經過,才視爲經過
* 三、部分過濾器可指定參數,如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緩存管理器;
* 須要注入對應的其它的實體類中:
* 一、安全管理器: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來作認證就足夠了。

相關文章
相關標籤/搜索