Shiro結合JWT實現單點登入
阿新 • • 發佈:2018-12-12
簡述
Apache Shiro是java的一個安全框架,Shiro可以幫助我們完成認證、授權、加密、會話管理、與Web整合、快取等。而且Shiro的API也比較簡單,這裡我們就不進行過多的贅述,想要詳細瞭解Shiro的,推薦看開濤的部落格(點這裡)
在Shiro的強大許可權管理的基礎上,我們實現單點登入就容易了很多,結合我上篇部落格所講的JSON Web Token(推薦先看這篇部落格)就可以完成單點登入系統。
實現過程
在使用Shiro實現登入的時候,將登入成功的資訊包括Token資訊返回給前端,前端在請求後臺時,將Token資訊存入請求頭中。配置自定義攔截器,攔截所有URL請求,取出請求頭資訊中的Token資訊,對Token資訊進行驗證,對於redis中存在的登入時生成的Token資訊,如果Token資訊正確,則確認該使用者已經登入,否則拒絕請求,返回401錯誤。
1.引入所需jar包
<!--json-web-token--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> </dependency> <!--shiro--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> </dependency>
2.登入認證
要實現單點登入功能,首先要完成的就是登入功能,這裡我們使用Shiro的認證來完成登入。
2.1 spring-shiro的配置檔案
<!--配置SecurityManager--> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realms"> <list> <ref bean="shiroRealm"/> </list> </property> </bean> <!--配置Realm--> <!--直接配置實現了org.apache.shiro.realm.Realm介面的bean--> <bean id="shiroRealm" class="com.why.authority.realms.ShiroRealm"> <!-- 憑證匹配器:配置登入驗證所使用的加密演算法--> <property name="credentialsMatcher"> <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"> <property name="hashAlgorithmName" value="sha-512"/> <property name="hashIterations" value="1024"/> </bean> </property> </bean> <!-- shiro攔截器--> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/user/index"/> <!--配置哪些頁面需要受保護 1. anon 可以被匿名訪問 2. authc 必須認證(登入)後才可能訪問的頁面 3. logout 登出 --> <!--自定義filters,該攔截器即為實現單點登入的攔截器--> <property name="filters"> <map> <entry key="acf"> <bean class="com.why.authority.filter.AccessingControlFilter"/> </entry> </map> </property> <property name="filterChainDefinitions"> <value> /user/index=anon /user/login=anon /user/content= acf /** = acf </value> </property> </bean>
2.2 登入方法
Controller:
@RequestMapping(value = {"/login"}, method = RequestMethod.POST)
@ResponseBody
public WhyResult content(@RequestParam("usercode") String usercode, @RequestParam("password") String password) {
String userInfoKey = "aum:user:" + usercode;
String tokenKey = "aum:token:"+usercode;
try {
if(StringUtils.isBlank(usercode) || StringUtils.isBlank(password)){
throw new UnknownAccountException();
}
//1. 執行登入
//把使用者名稱和密碼封裝為UsernamePasswordToken物件
UsernamePasswordToken token = new UsernamePasswordToken(usercode, password);
SecurityUtils.getSubject().login(token);
//2.獲取使用者資訊userEntity,redis中不存在則存入redis
UserEntity userEntity = new UserEntity();
//2.1 從redis中獲取或從資料庫中獲取
String strUserInfo = JedisCacheUtil.get(userInfoKey);
if (!StringUtils.isBlank(strUserInfo)) {
userEntity = JacksonJsonUntil.jsonToPojo(strUserInfo, UserEntity.class);
} else {
userEntity = addUserInfoToRedis(usercode, userInfoKey);
}
//3.生成Token資訊並儲存到redis
LoginEntity loginEntity = addTokenToRedis(userEntity,tokenKey);
return WhyResult.build(200,"登入成功!",loginEntity);
//所有認證異常的父類
} catch (AuthenticationException e) {
logger.error("登入失敗!",e);
return WhyResult.build(401,"使用者名稱或密碼錯誤!");
}
}
自定義Realm
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//1.把AuthenticationToken轉換為UsernamePasswordToken
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
//2.從UsernamePasswordToken中獲取userCode
String userCode = usernamePasswordToken.getUsername();
String userInfoKey = "aum:user:" + userCode;
UserEntity userEntity;
//3.獲取使用者資訊userEntity
//3.1 從redis中獲取
String strUserInfo;
try {
strUserInfo = JedisCacheUtil.get(userInfoKey);
if (!StringUtils.isBlank(strUserInfo)) {
userEntity = JacksonJsonUntil.jsonToPojo(strUserInfo, UserEntity.class);
} else {
userEntity = addUserAndGetUser(userCode, userInfoKey);
}
} catch (Exception e) {
userEntity = addUserAndGetUser(userCode, userInfoKey);
}
//6.根據使用者的情況,來構建AuthenticationInfo物件並返回
String credentials = userEntity.getPassword();
//使用ByteSource.Util.bytes()來計算鹽值
ByteSource credentialsSalt = ByteSource.Util.bytes(userCode);
return new SimpleAuthenticationInfo(userEntity, credentials, credentialsSalt, getName());
}
3. 自定義攔截器
該攔截器是在spring-shiro.xml檔案中配置的自定義攔截器,原理就是攔截每個請求,驗證URL請求頭資訊中的Token資訊是否過期,是否被篡改。
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
//是否驗證通過
boolean bool = false;
try {
HttpServletRequest req = WebUtils.toHttp(servletRequest);
String firstLoginToken = req.getParameter("token");
//從token中獲得資訊
Claims claims = TokenUtil.getClaims(firstLoginToken);
String userCode = claims.getSubject();
String userId = claims.getId();
String redisLoginKey = "aum:token:" + userCode;
String redisToken = JedisCacheUtil.get(redisLoginKey);
if(!StringUtils.isBlank(redisToken)){
String[] arrayRedisToken = redisToken.split("@");
//將使用者傳過來的token和redis中的做對比,若一樣,認為已經登入
if (arrayRedisToken[0].equals(firstLoginToken)) {
//比較這次訪問與登入的時間間隔有多少分鐘,如果大於5分鐘,則更新redis中的上次訪問時間資訊,將過期時間從新設定為30分鐘
long diffMin = TokenUtil.CompareTime(arrayRedisToken[1]);
if (diffMin >= 5) {
String currentAccessTime = PasswordUtil.base64Encoede(String.valueOf(System.currentTimeMillis()));
//更新redis中的token登入資訊
JedisCacheUtil.set(redisLoginKey, arrayRedisToken[0] + "@" + currentAccessTime, 30 * 60);
}
bool=true;
}
}
} catch (Exception e) {
return bool;
}
return bool;
}
至此為止,關鍵程式碼已經展示完了,現在實現的僅僅是最基礎的單點登入,還需要進行更多的安全檢查和驗證,這裡就不介紹了。
--------------------- 本文來自 王洪玉 的CSDN 部落格 ,全文地址請點選:https://blog.csdn.net/why15732625998/article/details/78647375?utm_source=copy