私聊我做畢設(shè)或者實(shí)驗(yàn)課題。
之前在網(wǎng)上查找一些關(guān)于shiro整合jwt的案例悔常,都比較繁瑣影斑,不適合新手學(xué)習(xí),自己把網(wǎng)上的案例學(xué)習(xí)一遍机打,做了一個(gè)類似矫户,也更加方便了解的權(quán)限管理功能。
數(shù)據(jù)庫(kù)設(shè)計(jì)残邀,網(wǎng)上一些案例都是設(shè)計(jì)好幾個(gè)表關(guān)聯(lián)皆辽,不容易理解,這里我就設(shè)計(jì)了一個(gè)表芥挣,膳汪,如果用戶角色是admin,后面會(huì)設(shè)計(jì)只有admin角色才能訪問的方法九秀,pression設(shè)計(jì)vip權(quán)限和normal權(quán)限,后面也會(huì)設(shè)計(jì)帶有vip權(quán)限或者normal權(quán)限才能訪問的方法粘我。
user
實(shí)驗(yàn)步驟
1.導(dǎo)入依賴
<dependency>
<!-- shiro-->
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
2.相關(guān)類的設(shè)計(jì)
shiroconfig,shirio的相關(guān)配置文件鼓蜒,都是固定的模板痹换,我們主要學(xué)習(xí)ShiroFilterFactoryBean 處理攔截資源文件問題,在里面我們添加自己的過濾器都弹,所有的請(qǐng)求都會(huì)經(jīng)過自定義的過濾器進(jìn)行過濾
@Configuration
public class ShiroConfig {
@Bean("securityManager")
public DefaultWebSecurityManager getManager(MyRealm realm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
// 使用自己的realm
manager.setRealm(realm);
/**
* 關(guān)閉shiro自帶的session娇豫,詳情見文檔
* http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
manager.setSubjectDAO(subjectDAO);
return manager;
}
@Bean("shiroFilter")
public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
// 添加自己的過濾器并且取名為jwt
Map<String, Filter> filterMap = new HashMap<>(4);
filterMap.put("jwt", new JWTFilter());
factoryBean.setFilters(filterMap);
factoryBean.setSecurityManager(securityManager);
factoryBean.setUnauthorizedUrl("/401");
/**
* 自定義url規(guī)則
*/
Map<String, String> filterRuleMap = new HashMap<>(4);
// 所有請(qǐng)求通過我們自己的JWT Filter
filterRuleMap.put("/**", "jwt");
// 訪問401和404頁(yè)面不通過我們的Filter
filterRuleMap.put("/401", "anon");
factoryBean.setFilterChainDefinitionMap(filterRuleMap);
return factoryBean;
}
/**
* 下面的代碼是添加注解支持
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 強(qiáng)制使用cglib,防止重復(fù)代理和可能引起代理出錯(cuò)的問題
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
JWTFilter畅厢,自定義的過濾器,當(dāng)發(fā)送過來(lái)的請(qǐng)求有攜帶token信息冯痢,就會(huì)執(zhí)行executeLogin方法,進(jìn)入到自定義的Realm進(jìn)行授權(quán)和認(rèn)證的功能框杜,因?yàn)槭且胨说哪0迤珠梗杂行┑胤讲恍枰梢宰⑨尩簦瑢?duì)跨域的支持暫時(shí)用不到咪辱。這里我在每一個(gè)方法后面都添加一個(gè)輸出語(yǔ)句是為了方便對(duì)程序運(yùn)行過程的理解振劳。
public class JWTFilter extends BasicHttpAuthenticationFilter {
private Logger LOGGER = LoggerFactory.getLogger(this.getClass());
/**
* 判斷用戶是否想要登入。
* true:是要登錄
*
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
System.out.println("*****進(jìn)入isLoginAttempt");
HttpServletRequest req = (HttpServletRequest) request;
String authorization = req.getHeader("token");
return authorization != null;
}
/**
*
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
//得到token信息
String authorization = httpServletRequest.getHeader("token");
JWTToken token = new JWTToken(authorization);
// 提交給realm進(jìn)行登入油狂,如果錯(cuò)誤他會(huì)拋出異常并被捕獲
getSubject(request, response).login(token);
// 如果沒有拋出異常則代表登入成功历恐,返回true
return true;
}
/**
* 這里我們?cè)敿?xì)說明下為什么最終返回的都是true,即允許訪問
* 例如我們提供一個(gè)地址 GET /article
* 登入用戶和游客看到的內(nèi)容是不同的
* 如果在這里返回了false专筷,請(qǐng)求會(huì)被直接攔截弱贼,用戶看不到任何東西
* 所以我們?cè)谶@里返回true,Controller中可以通過 subject.isAuthenticated() 來(lái)判斷用戶是否登入
* 如果有些資源只有登入用戶才能訪問磷蛹,我們只需要在方法上面加上 @RequiresAuthentication 注解即可
* 但是這樣做有一個(gè)缺點(diǎn)吮旅,就是不能夠?qū)ET,POST等請(qǐng)求進(jìn)行分別過濾鑒權(quán)(因?yàn)槲覀冎貙懥斯俜降姆椒?,但實(shí)際上對(duì)應(yīng)用影響不大
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if (isLoginAttempt(request, response)) {
try {
System.out.println("執(zhí)行executeLogin");
executeLogin(request, response);
} catch (Exception e) {
response401(request, response);
}
}
System.out.println("沒有執(zhí)行executeLogin");
return true;
}
// /**
// * 對(duì)跨域提供支持
// */
// @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);
// }
/**
* 將非法請(qǐng)求跳轉(zhuǎn)到 /401
*/
private void response401(ServletRequest req, ServletResponse resp) {
try {
HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
httpServletResponse.sendRedirect("/401");
} catch (IOException e) {
LOGGER.error(e.getMessage());
}
}
}
當(dāng)發(fā)送過來(lái)的請(qǐng)求攜帶token信息鸟辅,就會(huì)進(jìn)入到自定義Realm類中進(jìn)行認(rèn)證,繼承AuthorizingRealm 類并重寫里面的認(rèn)證和授權(quán)的方法。首先觀察doGetAuthenticationInfo方法莺葫,這里我們首先獲得token信息匪凉,然后通過JwtUtils工具類對(duì)token信息進(jìn)行解析,獲得當(dāng)前用戶的姓名捺檬,然后去數(shù)據(jù)庫(kù)中查到當(dāng)前用戶再层,如果當(dāng)前用戶存在,則把當(dāng)前用戶信息保存到 SimpleAuthenticationInfo對(duì)象中堡纬,后面做授權(quán)要用到(第一個(gè)參數(shù)用來(lái)保存當(dāng)前用戶信息聂受,也可以保存token信息,不唯一烤镐,自己定義)
@Configuration
public class MyRealm extends AuthorizingRealm {
@Autowired
private UserService userServicel;
/*
必須要加蛋济,不然程序會(huì)報(bào)錯(cuò)
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}
/*
這里的PrincipalCollection對(duì)應(yīng)SimpleAuthenticationInfo的第一個(gè)參數(shù)
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
System.out.println("執(zhí)行————————》AuthorizationInfo");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// User user=(User)principal.getPrimaryPrincipal();
// System.out.println(user);
String username = JwtUtils.getUsername(principal.toString());
System.out.println(username);
User user = userServicel.getOne(new QueryWrapper<User>().eq("username", username));
info.addRole(user.getRole());
info.addStringPermission(user.getPression());
return info;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
System.out.println("執(zhí)行————————》AuthenticationInfo");
String token = (String) auth.getCredentials();
System.out.println("token信息"+token);
// 解密獲得username,用于和數(shù)據(jù)庫(kù)進(jìn)行對(duì)比
String username = JwtUtils.getUsername(token);
System.out.println(username);
if (username == null) {
throw new AuthenticationException("token invalid");
}
User user = userServicel.getOne(new QueryWrapper<User>().eq("username", username));
if (user == null) {
throw new AuthenticationException("User didn't existed!");
}
return new SimpleAuthenticationInfo(token, token, "my_realm");
}
}
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;
}
}
public class JwtUtils {
//定義兩個(gè)常量炮叶,1.設(shè)置過期時(shí)間 2.密鑰(隨機(jī)碗旅,由公司生成)
public static final long EXPIRE = 1000 * 60 * 60 * 24;
public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";
//生成token字符串渡处,用戶id和名稱(可以寫多個(gè))
public static String getJwtToken(String username, String password){
String JwtToken = Jwts.builder()
//設(shè)置token的頭信息
.setHeaderParam("typ", "JWT")
.setHeaderParam("alg", "HS256")
//設(shè)置過期時(shí)間
.setSubject("user")
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
//設(shè)置token的主題部分
.claim("username", username)
.claim("password", password)
//簽名哈希
.signWith(SignatureAlgorithm.HS256, APP_SECRET)
.compact();
return JwtToken;
}
/**
* 判斷token是否存在與有效
* @param jwtToken
* @return
*/
public static boolean checkToken(String jwtToken) {
if(StringUtils.isEmpty(jwtToken)) return false;
try {
//驗(yàn)證是否有效的token
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 根據(jù)token信息得到username
* @param jwtToken
* @return
*/
public static String getUsername(String jwtToken) {
//驗(yàn)證是否有效的token
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
//得到字符串的主題部分
Claims claims = claimsJws.getBody();
return (String)claims.get("username");
}
/**
* 判斷token是否存在與有效
* @param request
* @return
*/
public static boolean checkToken(HttpServletRequest request) {
try {
String jwtToken = request.getHeader("token");
if(StringUtils.isEmpty(jwtToken)) return false;
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 根據(jù)token獲取會(huì)員id
* @param request
* @return
*/
public static String getMemberIdByJwtToken(HttpServletRequest request) {
String jwtToken = request.getHeader("token");
if(StringUtils.isEmpty(jwtToken)) return "";
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
//得到字符串的主題部分
Claims claims = claimsJws.getBody();
return (String)claims.get("username");
}
}
這里我們可以對(duì)認(rèn)證功能進(jìn)行試驗(yàn)。使用postman測(cè)試祟辟,創(chuàng)建一個(gè)loginController類医瘫,得到當(dāng)前用戶的token信息。
@PostMapping("/login")
public R login(@RequestBody JSONObject requestJson){
System.out.println(requestJson);
String username = requestJson.getString("username");
String password = requestJson.getString("password");
String token = JwtUtils.getJwtToken(username, password);
return R.ok().data("token",token);
}
image.png
我們隨便寫一個(gè)請(qǐng)求旧困,用戶測(cè)試
@GetMapping("user")
//@RequiresRoles("admin")
//@RequiresRoles(logical = Logical.OR, value = {"user", "admin"})
public R getUser(){
return R.ok();
}
沒有攜帶token信息
image.png
查看輸出臺(tái)
image.png
攜帶token信息
image.png
查看輸出臺(tái)醇份,執(zhí)行了AuthenticationInfo方法
image.png
現(xiàn)在我們就可以開始做授權(quán)認(rèn)證的功能,觀察doGetAuthorizationInfo方法吼具,這里的PrincipalCollection對(duì)應(yīng)SimpleAuthenticationInfo的第一個(gè)參數(shù)僚纷,通過principal獲取當(dāng)前用戶,并授予權(quán)限馍悟,使用addRole添加用戶角色畔濒,使用 addStringPermission添加用戶的權(quán)限,還需要了解2個(gè)注解锣咒,@RequiresRoles 授權(quán)方法中給用戶添加角色侵状,@RequiresPermissions 授權(quán)方法中給用戶添加權(quán)限。
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
System.out.println("執(zhí)行————————》AuthorizationInfo");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// User user=(User)principal.getPrimaryPrincipal();
// System.out.println(user);
String username = JwtUtils.getUsername(principal.toString());
System.out.println(username);
User user = userServicel.getOne(new QueryWrapper<User>().eq("username", username));
info.addRole(user.getRole());
info.addStringPermission(user.getPression());
return info;
}
我們?cè)谥暗恼?qǐng)求方法中添加@RequiresRoles注解毅整,可以從控制臺(tái)中看到執(zhí)行了授權(quán)的方法趣兄,但是因?yàn)楫?dāng)前用戶的角色是guest,所以會(huì)報(bào)錯(cuò)悼嫉。
@GetMapping("user")
@RequiresRoles("admin")
//@RequiresRoles(logical = Logical.OR, value = {"user", "admin"})
public R getUser(){
return R.ok();
}
image.png
image.png
修改用戶的角色為guest,再發(fā)送請(qǐng)求
@GetMapping("user")
@RequiresRoles("guest")
//@RequiresRoles(logical = Logical.OR, value = {"user", "admin"})
public R getUser(){
return R.ok();
}
訪問成功
image.png