shiro整合jwt
這篇文章參考了這個(gè)網(wǎng)址:https://github.com/HowieYuan/Shiro-SpringBoot
一陪腌、添加依賴
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.qianfeng</groupId>
<artifactId>shiro-jwt</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>shiro-jwt</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
這是一個(gè)springboot項(xiàng)目娜扇,除了常規(guī)的springboot的依賴和數(shù)據(jù)庫的依賴选调,最重要的是shiro和jwt的依賴窃肠,分別是shiro-spring與java-jwt。
二士骤、項(xiàng)目結(jié)構(gòu)
[圖片上傳失敗...(image-6f4263-1590589772559)]
如上圖所示忆蚀,config包是配置包,ShiroConfig是Shiro的配置類嘁扼,用來對(duì)shiro進(jìn)行一些自定義配置信粮。filter是過濾器,因?yàn)槲覀兪褂昧薺wt所以需要自定義過濾器趁啸。然后是model强缘,由于這是一個(gè)前后端分離的項(xiàng)目,在向前端返回?cái)?shù)據(jù)的時(shí)候返回的是json格式的數(shù)據(jù)不傅,同時(shí)還要封裝一些狀態(tài)信息等旅掂,所以自定義這個(gè)類用來完成對(duì)狀態(tài)、對(duì)象的封裝访娶。shiro包下面的兩個(gè)類一個(gè)JWTToken是自定義的Token商虐,MyRealm是自定義的Realm。util包下的JWTUtil用來生成并校驗(yàn)token。
三称龙、JWTUtil
package com.qianfeng.util;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.io.UnsupportedEncodingException;
import java.util.Date;
/**
* @author huwen
*/
public class JWTUtil {
/**
* 設(shè)置過期時(shí)間24小時(shí)
*/
private static final long EXPIRE_TIME = 1000*60*60*24;
/**
* 設(shè)置密鑰
*/
private static final String SECRET = "shiro+jwt";
/**
* 根據(jù)用戶名創(chuàng)建一個(gè)token
* @param username 用戶名
* @return 返回的token字符串
*/
public static String createToken(String username){
try {
//將當(dāng)前時(shí)間的毫秒數(shù)和設(shè)置的過期時(shí)間相加生成一個(gè)新的時(shí)間
Date date = new Date(System.currentTimeMillis()+EXPIRE_TIME);
//由密鑰創(chuàng)建一個(gè)指定的算法
Algorithm algorithm = Algorithm.HMAC256(SECRET);
return JWT.create()
//附帶username信息
.withClaim("username",username)
//附帶過期時(shí)間
.withExpiresAt(date)
//使用指定的算法進(jìn)行標(biāo)記
.sign(algorithm);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return null;
}
}
/**
* 驗(yàn)證token是否正確
* @param token 前端傳過來的token
* @param username 用戶名
* @return 返回boolean
*/
public static boolean verify(String token,String username){
try {
//獲取算法
Algorithm algorithm = Algorithm.HMAC256(SECRET);
//生成JWTVerifier
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("username",username)
.build();
//驗(yàn)證token
verifier.verify(token);
return true;
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return false;
}
}
/**
* 從token中獲得username留拾,無需secret
* @param token token
* @return username
*/
public static String getUsername(String token){
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
return null;
}
}
}
這個(gè)類用來完成一些對(duì)token的操作:創(chuàng)建token、驗(yàn)證token鲫尊、從token中獲得username痴柔。創(chuàng)建token的時(shí)候需要指定token的過期時(shí)間,以及secret疫向,同樣咳蔚,驗(yàn)證的時(shí)候也需要secret。最后搔驼,從token中獲取username并不需要secret谈火。
四、JWTFilter
package com.qianfeng.filter;
import com.qianfeng.shiro.JWTToken;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
public class JWTFilter extends BasicHttpAuthenticationFilter {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
//判斷請(qǐng)求頭是否帶上“Token”
if(isLoginAttempt(request, response)){
//如果存在舌涨,則執(zhí)行executeLogin方法登入糯耍,檢查token是否正確
try {
executeLogin(request, response);
return true;
} catch (Exception e) {
responseError(response,e.getMessage());
}
}
//如果沒有token,則可能是執(zhí)行登錄操作或者是游客狀態(tài)訪問囊嘉,無需檢查token温技,直接返回true
return true;
}
/**
* 將非法請(qǐng)求跳轉(zhuǎn)到 /unauthorized/**
* @param response
* @param message
*/
private void responseError(ServletResponse response, String message) {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
try {
message = URLEncoder.encode(message,"UTF-8");
httpServletResponse.sendRedirect("/unauthorized/"+message);
} catch (IOException e) {
logger.error(e.getMessage());
}
}
/**
* 判斷用戶是否想要登入
* 檢測(cè)header里面是否包含Token字段
* @param request request
* @param response response
* @return boolean
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String token = req.getHeader("Token");
return token != null;
}
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader("Token");
JWTToken jwtToken = new JWTToken(token);
//提交給realm進(jìn)行登入,如果錯(cuò)誤就會(huì)拋出異常并被捕獲
getSubject(request, response).login(jwtToken);
return true;
}
/**
* 對(duì)跨域訪問提供支持
* @param request
* @param response
* @return
* @throws Exception
*/
@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"));
// 跨域時(shí)會(huì)首先發(fā)送一個(gè)option請(qǐng)求扭粱,這里我們給option請(qǐng)求直接返回正常狀態(tài)
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
}
這個(gè)類繼承了BasicHttpAuthenticationFilter并重寫了里面的部分方法舵鳞。最重要的是executeLogin這個(gè)方法,這個(gè)方法從請(qǐng)求頭中獲取token這個(gè)字段然后構(gòu)造出一個(gè)JWTToken對(duì)象進(jìn)行登錄琢蛤,實(shí)際上就是提交給MyRealm蜓堕,和我們之前的UsernamePasswordToken登錄的方式一樣。
五博其、JWTToken
package com.qianfeng.shiro;
import org.apache.shiro.authc.AuthenticationToken;
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;
}
}
這個(gè)類實(shí)現(xiàn)了AuthenticationToken認(rèn)證Token這個(gè)接口套才,UsernamePasswordToken也是實(shí)現(xiàn)了這個(gè)接口,都可以用作認(rèn)證贺奠。
六霜旧、MyRealm
package com.qianfeng.shiro;
import com.qianfeng.pojo.Employee;
import com.qianfeng.pojo.Permission;
import com.qianfeng.pojo.Roles;
import com.qianfeng.service.EmployeeService;
import com.qianfeng.util.JWTUtil;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Component("myRealm")
public class MyRealm extends AuthorizingRealm {
@Resource
private EmployeeService employeeService;
/**
* 必須重寫此方法,否則會(huì)報(bào)錯(cuò)
* @param token
* @return
*/
@Override
public boolean supports(AuthenticationToken token){
return token instanceof JWTToken;
}
/**
* 授權(quán)方法
* @param principalCollection principal的集合儡率,可以理解為各種用戶身份的集合,比如用戶名以清、郵箱儿普、手機(jī)號(hào)等
* @return 返回的是授權(quán)信息,包括角色與權(quán)限
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String username = JWTUtil.getUsername(principalCollection.toString());
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
List<Roles> roles = employeeService.getAllRolesByEmpName(username);
Set<String> roleSet = new HashSet<>();
Set<String> permissionSet = new HashSet<>();
for (Roles role : roles) {
roleSet.add(role.getRoleName());
}
List<Permission> permissions = employeeService.getAllPermissionsByEmpName(username);
for (Permission permission : permissions) {
permissionSet.add(permission.getPermName());
}
info.setRoles(roleSet);
info.setStringPermissions(permissionSet);
return info;
}
/**
* 這個(gè)方法用于認(rèn)證
* @param authenticationToken 用戶名與密碼
* @return 認(rèn)證信息
* @throws AuthenticationException 可能引發(fā)的異常
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//獲得token
String token = (String) authenticationToken.getCredentials();
//從token中獲得username
String username = JWTUtil.getUsername(token);
//如果username為空或者驗(yàn)證不匹配
if(username == null||!JWTUtil.verify(token,username)){
throw new AuthenticationException("token認(rèn)證失敗!");
}
String password = employeeService.getPassword(username);
//如果沒有查詢到用戶名對(duì)應(yīng)的密碼
if(password==null){
throw new AuthenticationException("該用戶不存在");
}
return new SimpleAuthenticationInfo(token,token,"MyRealm");
}
}
MyRealm繼承了AuthorizingRealm掷倔,授權(quán)Realm眉孩,重寫其中的認(rèn)證和授權(quán)方法。
七、ShiroConfig
package com.qianfeng.config;
import com.qianfeng.filter.JWTFilter;
import com.qianfeng.shiro.MyRealm;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//創(chuàng)建自定義過濾器
Map<String, Filter> filterMap = new LinkedHashMap<>();
//將JWTFilter命名為jwt
filterMap.put("jwt",new JWTFilter());
shiroFilterFactoryBean.setFilters(filterMap);
//設(shè)置securityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
//設(shè)置無權(quán)限時(shí)跳轉(zhuǎn)的url
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized/無權(quán)限");
shiroFilterFactoryBean.setLoginUrl("/login");
Map<String,String> filterRuleMap = new HashMap<>(2);
//所有請(qǐng)求通過我們自己的過濾器
filterRuleMap.put("/**","jwt");
//匿名用戶可以訪問的url
filterRuleMap.put("/unauthorized/**","anon");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterRuleMap);
return shiroFilterFactoryBean;
}
@Bean
public SecurityManager securityManager(MyRealm myRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myRealm);
//關(guān)閉shiro自帶的session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 強(qiáng)制使用cglib浪汪,防止重復(fù)代理和可能引起代理出錯(cuò)的問題
// https://zhuanlan.zhihu.com/p/29161098
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
}
這個(gè)類是Shiro的配置類巴柿,設(shè)置好我們自定義的 filter,并使所有請(qǐng)求通過我們的過濾器死遭,除了我們用于處理未認(rèn)證請(qǐng)求的 /unauthorized/**
八广恢、權(quán)限控制注解
主要通過shiro的@RequiresRoles和@RequiresPermissions注解進(jìn)行權(quán)限控制,這兩個(gè)注解放在controller的返回方法上呀潭,如果不具有相應(yīng)的角色或權(quán)限就會(huì)拋出異常