Part2 今日主題:springboot整合shiro
shiro是一款安全框架,能夠控制登陸,能夠保證安全,對於咱們來講一些接口的安全必須經過安全框架來控制,防止別人蓄意刷接口。前端
我仍是要講一下他的原理吧,若是這我的沒有通過登陸頁面,去訪問其餘頁面,shiro框架會將請求轉發到登陸頁面去,讓這我的登陸,登陸成功以後會給前端一個token,前端將token保存下來,每次去請求項目中的其餘頁面或者接口的時候,須要將token攜帶到請求頭中,給後端,請求首先會通過攔截器,攔截器會對token進行判斷,判斷該token是否有效,若是有效,則放行,不然直接攔截了。java
咱們須要shiro和jwt的依賴web
<!-- 自動依賴導入 shiro-core 和 shiro-web -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
<!--jwt-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.11.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
這裏用到jwt,簡單的理解一下,他就是一款加密工具,登陸成功以後,咱們須要將一些信息發給前端,可是咱們不可能明文發送,須要對這些信息進行加密,這個token就是一些加密的信息。算法
1.首先寫一個jwt加密解密的工具類spring
/*
* 總的來講,工具類中有三個方法
* 獲取JwtToken,獲取JwtToken中封裝的信息,判斷JwtToken是否存在
* 1. encode(),參數是=簽發人,存在時間,一些其餘的信息=。返回值是JwtToken對應的字符串
* 2. decode(),參數是=JwtToken=。返回值是荷載部分的鍵值對
* 3. isVerify(),參數是=JwtToken=。返回值是這個JwtToken是否存在
* */
public class JwtUtil {
//建立默認的祕鑰和算法,供無參的構造方法使用
private static final String defaultbase64EncodedSecretKey = "java後端指南";
private static final SignatureAlgorithm defaultsignatureAlgorithm = SignatureAlgorithm.HS256;
public JwtUtil() {
this(defaultbase64EncodedSecretKey, defaultsignatureAlgorithm);
}
private final String base64EncodedSecretKey;
private final SignatureAlgorithm signatureAlgorithm;
public JwtUtil(String secretKey, SignatureAlgorithm signatureAlgorithm) {
this.base64EncodedSecretKey = Base64.encodeBase64String(secretKey.getBytes());
this.signatureAlgorithm = signatureAlgorithm;
}
/*
*這裏就是產生jwt字符串的地方
* jwt字符串包括三個部分
* 1. header
* -當前字符串的類型,通常都是「JWT」
* -哪一種算法加密,「HS256」或者其餘的加密算法
* 因此通常都是固定的,沒有什麼變化
* 2. payload
* 通常有四個最多見的標準字段(下面有)
* iat:簽發時間,也就是這個jwt何時生成的
* jti:JWT的惟一標識
* iss:簽發人,通常都是username或者userId
* exp:過時時間
*
* */
public String encode(String iss, long ttlMillis, Map<String, Object> claims) {
//iss簽發人,ttlMillis生存時間,claims是指還想要在jwt中存儲的一些非隱私信息
if (claims == null) {
claims = new HashMap<>();
}
long nowMillis = System.currentTimeMillis();
JwtBuilder builder = Jwts.builder()
.setClaims(claims)
.setId(UUID.randomUUID().toString())//2. 這個是JWT的惟一標識,通常設置成惟一的,這個方法能夠生成惟一標識
.setIssuedAt(new Date(nowMillis))//1. 這個地方就是以毫秒爲單位,換算當前系統時間生成的iat
.setSubject(iss)//3. 簽發人,也就是JWT是給誰的(邏輯上通常都是username或者userId)
.signWith(signatureAlgorithm, base64EncodedSecretKey);//這個地方是生成jwt使用的算法和祕鑰
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);//4. 過時時間,這個也是使用毫秒生成的,使用當前時間+前面傳入的持續時間生成
builder.setExpiration(exp);
}
return builder.compact();
}
//至關於encode的方向,傳入jwtToken生成對應的username和password等字段。Claim就是一個map
//也就是拿到荷載部分全部的鍵值對
public Claims decode(String jwtToken) {
// 獲得 DefaultJwtParser
return Jwts.parser()
// 設置簽名的祕鑰
.setSigningKey(base64EncodedSecretKey)
// 設置須要解析的 jwt
.parseClaimsJws(jwtToken)
.getBody();
}
//判斷jwtToken是否合法
public boolean isVerify(String jwtToken) {
//這個是官方的校驗規則,這裏只寫了一個」校驗算法「,能夠本身加
Algorithm algorithm = null;
switch (signatureAlgorithm) {
case HS256:
algorithm = Algorithm.HMAC256(Base64.decodeBase64(base64EncodedSecretKey));
break;
default:
throw new RuntimeException("不支持該算法");
}
JWTVerifier verifier = JWT.require(algorithm).build();
verifier.verify(jwtToken); // 校驗不經過會拋出異常
//判斷合法的標準:1. 頭部和荷載部分沒有篡改過。2. 沒有過時
return true;
}
2.類型UsernamePasswordToken的類數據庫
/**
* 這個就相似UsernamePasswordToken
*/
public class JwtToken implements AuthenticationToken {
//返回值都是jwt
private String jwt;
public JwtToken(String jwt) {
this.jwt = jwt;
}
/**
* 相似是用戶名
* @return
*/
@Override
public Object getPrincipal() {
return jwt;
}
/**
* 相似密碼
* @return
*/
@Override
public Object getCredentials() {
return jwt;
}
}
3.建立JwtDefaultSubjectFactory,來實現不保存sessionapache
public class JwtDefaultSubjectFactory extends DefaultWebSubjectFactory {
@Override
public Subject createSubject(SubjectContext context) {
// 不建立 session
context.setSessionCreationEnabled(false);
return super.createSubject(context);
}
}
4.建立AuthorizingRealm
json
@Slf4j
public class JwtRealm extends AuthorizingRealm {
/*
* 多重寫一個support
* 標識這個Realm是專門用來驗證JwtToken
* 不負責驗證其餘的token(UsernamePasswordToken)
* */
@Override
public boolean supports(AuthenticationToken token) {
//這個token就是從過濾器中傳入的jwtToken
return token instanceof JwtToken;
}
//受權
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
//認證
//這個token就是從過濾器中傳入的jwtToken
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String jwt = (String) token.getPrincipal();
if (jwt == null) {
throw new NullPointerException("jwtToken 不容許爲空");
}
//判斷
JwtUtil jwtUtil = new JwtUtil();
if (!jwtUtil.isVerify(jwt)) {
throw new UnknownAccountException();
}
//下面是驗證這個user是不是真實存在的
String username = (String) jwtUtil.decode(jwt).get("username");//判斷數據庫中username是否存在
log.info("在使用token登陸"+username);
return new SimpleAuthenticationInfo(jwt,jwt,"JwtRealm");
//這裏返回的是相似帳號密碼的東西,可是jwtToken都是jwt字符串。還須要一個該Realm的類名
}
}
5.實現過濾器AccessControlFilter
後端
@Slf4j
public class JwtFilter extends AccessControlFilter {
/*
* 1. 返回true,shiro就直接容許訪問url
* 2. 返回false,shiro纔會根據onAccessDenied的方法的返回值決定是否容許訪問url
* */
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
log.warn("isAccessAllowed 方法被調用");
//這裏先讓它始終返回false來使用onAccessDenied()方法
return false;
}
/**
* 返回結果爲true代表登陸經過
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
log.warn("onAccessDenied 方法被調用");
//這個地方和前端約定,要求前端將jwtToken放在請求的Header部分
//因此之後發起請求的時候就須要在Header中放一個Authorization,值就是對應的Token
HttpServletRequest request = (HttpServletRequest) servletRequest;
String jwt = request.getHeader("Authorization");
log.info("請求的 Header 中藏有 jwtToken {}", jwt);
JwtToken jwtToken = new JwtToken(jwt);
/*
* 下面就是固定寫法
* */
try {
// 委託 realm 進行登陸認證
//因此這個地方最終仍是調用JwtRealm進行的認證
getSubject(servletRequest, servletResponse).login(jwtToken);
//也就是subject.login(token)
} catch (Exception e) {
e.printStackTrace();
onLoginFail(servletResponse);
//調用下面的方法向客戶端返回錯誤信息
return false;
}
return true;
//執行方法中沒有拋出異常就表示登陸成功
}
//登陸失敗時默認返回 401 狀態碼
private void onLoginFail(ServletResponse response) throws IOException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
httpResponse.getWriter().write("login error");
}
}
6.添加配置文件,實現某些路徑的認證和受權安全
//springBoot整合jwt實現認證有三個不同的地方,對應下面abc
@Configuration
public class ShiroConfig {
/*
* a. 告訴shiro不要使用默認的DefaultSubject建立對象,由於不能建立Session
* */
@Bean
public SubjectFactory subjectFactory() {
return new JwtDefaultSubjectFactory();
}
@Bean
public Realm realm() {
return new JwtRealm();
}
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm());
/*
* b
* */
// 關閉 ShiroDAO 功能
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
// 不須要將 Shiro Session 中的東西存到任何地方(包括 Http Session 中)
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
//禁止Subject的getSession方法
securityManager.setSubjectFactory(subjectFactory());
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager());
shiroFilter.setLoginUrl("/unauthenticated");
shiroFilter.setUnauthorizedUrl("/unauthorized");
/*
* c. 添加jwt過濾器,並在下面註冊
* 也就是將jwtFilter註冊到shiro的Filter中
* 指定除了login和logout以外的請求都先通過jwtFilter
* */
Map<String, Filter> filterMap = new HashMap<>();
//這個地方其實另外兩個filter能夠不設置,默認就是
filterMap.put("anon", new AnonymousFilter());
filterMap.put("jwt", new JwtFilter());
filterMap.put("logout", new LogoutFilter());
shiroFilter.setFilters(filterMap);
// 攔截器
Map<String, String> filterRuleMap = new LinkedHashMap<>();
filterRuleMap.put("/login", "anon");
filterRuleMap.put("/logout", "logout");
filterRuleMap.put("/**", "jwt");
shiroFilter.setFilterChainDefinitionMap(filterRuleMap);
return shiroFilter;
}
}
7.實現控制類
我這裏爲了省事,直接用的是一個固定的用戶名和密碼,你們能夠本身寫一個方法去數據庫查詢
@RequestMapping("/login")
public ResponseEntity<Map<String, String>> login(String username, String password) {
log.info("username:{},password:{}",username,password);
Map<String, String> map = new HashMap<>();
if (!"tom".equals(username) || !"123".equals(password)) {
map.put("msg", "用戶名密碼錯誤");
return ResponseEntity.ok(map);
}
JwtUtil jwtUtil = new JwtUtil();
Map<String, Object> chaim = new HashMap<>();
chaim.put("username", username);
String jwtToken = jwtUtil.encode(username, 5 * 60 * 1000, chaim);
map.put("msg", "登陸成功");
map.put("token", jwtToken);
return ResponseEntity.ok(map);
}
@RequestMapping("/testdemo")
public ResponseEntity<String> testdemo() {
return ResponseEntity.ok("java後端指南");
}
8.訪問 若是直接訪問其餘接口,可是沒有登陸的話就會報沒有登陸
首先是登陸成功,返給前端一個token而後咱們再攜帶token到後端去,訪問其餘接口