springboot2.0框架下使用Shiro
介紹
Shiro是Apache旗下的開(kāi)源項(xiàng)目,是一個(gè)簡(jiǎn)單易用的安全框架砾跃,提供包括認(rèn)證骏啰、授權(quán)、加密抽高、會(huì)話管理等諸多功能判耕。Shiro使用了比較簡(jiǎn)單易懂易于使用的授權(quán)方式撕予。Shiro屬于輕量級(jí)框架唱遭,配置簡(jiǎn)單遣总,應(yīng)用廣泛构蹬。在很多優(yōu)秀的開(kāi)源項(xiàng)目之中都有使用六水。
搭建springboot种远,網(wǎng)頁(yè)部分
創(chuàng)建項(xiàng)目
搭建springboot開(kāi)發(fā)環(huán)境
打開(kāi)idea盆耽,點(diǎn)擊文件(file)-->新建(new)-->新建文件(Project)
之后就會(huì)進(jìn)入idea的新建項(xiàng)目之中轰绵,這里點(diǎn)擊這個(gè)樹(shù)葉加一個(gè)開(kāi)關(guān)按鈕的這個(gè)標(biāo)志莹桅,之后點(diǎn)擊下一步就可以了
之后這個(gè)就是昌执,真正的配置界面,填好信息诈泼,就可以點(diǎn)擊下一步懂拾,這里面比較重要的是這個(gè)javaVersion這里,默認(rèn)是11版本厂汗。需要用戶自己根據(jù)實(shí)際情況選擇版本
在依賴選擇這里委粉,只選Web里面的spring web選項(xiàng)就可以了。然后點(diǎn)擊下一步娶桦。
這里一般只看上面就可以了贾节,項(xiàng)目名和項(xiàng)目位置最后面的名字可以不一致,不影響程序的運(yùn)行
點(diǎn)擊完成衷畦,再等maven項(xiàng)目下載所需的依賴就可以了栗涂。依賴下載完成,項(xiàng)目結(jié)構(gòu)如下祈争。(target在項(xiàng)目運(yùn)行時(shí)斤程,會(huì)生成)
添加依賴項(xiàng)
打開(kāi)pom.xml文件在<dependencies>標(biāo)簽內(nèi)添加依依賴
org.apache.shiro.shiro-all是本文中需要使用的的依賴
com.alibaba.fastjson是一個(gè)json格式文件處理工具,可能會(huì)用到
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-all</artifactId>
<version>1.6.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.74</version>
</dependency>
打開(kāi)idea右側(cè)的maven的,點(diǎn)擊如圖所示刷新按鈕忿墅,下載依賴包
搭建網(wǎng)頁(yè)前端內(nèi)容
用幾個(gè)前端網(wǎng)頁(yè)扁藕,對(duì)接后端數(shù)據(jù),方便調(diào)試疚脐。
第一個(gè)是首頁(yè)面(需要登錄才能進(jìn)入)
第二個(gè)是用戶的登錄頁(yè)面
第三個(gè)是提示用戶沒(méi)有授權(quán)的頁(yè)面亿柑,他會(huì)自動(dòng)跳轉(zhuǎn)到首頁(yè)面或者登錄頁(yè)面。
首先是第一個(gè)頁(yè)面棍弄,簡(jiǎn)單的使用html網(wǎng)頁(yè)寫一個(gè)大大的首頁(yè)望薄,并對(duì)其簡(jiǎn)單的美化一下,非常清楚
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>首頁(yè)</title>
</head>
<body style="margin: 0;padding: 0;border-style: none">
<h1 style="margin:0;padding: 0;text-align: center;background-color: #00ced1;color: #9400d3;font-size: 150px;text-shadow: 5px 5px 2px #6000a3;">
首頁(yè)主體
</h1>
</body>
</html>
然后是第二個(gè)頁(yè)面呼畸,登錄頁(yè)面痕支,使用form表單,action內(nèi)容千萬(wàn)不要使用域名(ip)+端口之后是路徑這種方式蛮原,則會(huì)導(dǎo)致每次提交表單卧须,都會(huì)導(dǎo)致session不一致,直接寫路徑就好瞬痘。還是死一樣故慈,簡(jiǎn)單的美化一下就好
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>系統(tǒng)登錄</title>
<style>
input[type="text"], input[type="password"] {
margin: 0 2px;
height: 35px;
width: 100%;
}
input[type="submit"], input[type="button"] {
height: 35px;
width: 80px;
}
td {
font-size: 20px;
text-align: right;
}
tr{
}
table {
padding: 0 20px 0 0 ;
width: 30%;
border-style: solid;
border-width: 1px;
}
</style>
</head>
<body>
<center>
<form action="/user/login" method="get" style="margin-top: 10%;" target="_self">
<table>
<tr>
<td>用戶名</td>
<td><input type="text" name="username"/></td>
</tr>
<tr>
<td>密碼</td>
<td><input type="password" name="password"/></td>
</tr>
<tr>
<td colspan="2"><input type="button" value="沒(méi)用的按鈕"style="margin-left:30%; ">
<input type="submit" value="登錄" style="margin-left:10%; "></td>
</tr>
</table>
</form>
</center>
</body>
</html>
之后就是用戶沒(méi)有權(quán)限時(shí)跳轉(zhuǎn)的頁(yè)面,這里設(shè)置為打開(kāi)頁(yè)面5秒后框全,自動(dòng)跳轉(zhuǎn)到首頁(yè)面或者登錄頁(yè)面察绷。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>你沒(méi)有該權(quán)限訪問(wèn)</title>
</head>
<body>
<div style="margin: 100px 0 0 40%;font-size: 30px">
<a id="aa123" href="/login.html">5秒后,跳轉(zhuǎn)登錄頁(yè)面....</a>
</div>
</body>
<script>
let time = 5;
let interval = setInterval(setText, 1000)
setTimeout(toLogin, 5000);
function toLogin() {
window.open("/login.html","_self")
}
function setText() {
time--;
document.getElementById("aa123").innerText = time + "秒后津辩,跳轉(zhuǎn)登錄頁(yè)面...."
if (time===0){
clearInterval(interval)
}
}
</script>
</html>
java部分
介紹
考慮到sql拆撼,mybatis,配置過(guò)于繁雜喘沿,所以項(xiàng)目中使用靜態(tài)的數(shù)據(jù)代替sql闸度。再模擬寫一個(gè)service層的部分。
啟動(dòng)項(xiàng)
在項(xiàng)目的啟動(dòng)項(xiàng)Application類中設(shè)置一個(gè)新注解蚜印,這里使用@ComponentScan注解來(lái)掃描注解類莺禁,為了省事,直接使用**號(hào)掃描
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan(basePackages = {
//掃描包窄赋,加載注解
"com.eelinker.shiro.**",
"com.eelinker.shiro.**.**",
"com.eelinker.shiro.**.**.**"
})
public class ShiroApplication {
public static void main(String[] args) {
SpringApplication.run(ShiroApplication.class, args);
}
}
Service層
User
首先需要?jiǎng)?chuàng)建一個(gè)實(shí)體類哟冬,User,用戶的數(shù)據(jù)信息封裝,get和set以及toString方法省略忆绰。這里面有一個(gè)Role的類是沒(méi)有定義的浩峡,代表的是用戶的訪問(wèn)權(quán)限。
public class User {
//用戶id
private int id;
//用戶名
private String username;
//用戶密碼
private String password;
//用戶權(quán)限
private Role role;
}
Role
這里設(shè)定了老板權(quán)限错敢,員工權(quán)限翰灾,檢查人員權(quán)限,以及顧客權(quán)限三大類型,詳細(xì)的權(quán)限還分為管理權(quán)(administration)纸淮,制作權(quán)(make)平斩,檢查權(quán)(check)
顧客只能訪問(wèn)最基本的訪問(wèn)權(quán)限,其他的人都默認(rèn)擁有顧客的訪問(wèn)權(quán)限萎馅,這些訪問(wèn)權(quán)限都需要登錄双戳,不登錄都會(huì)默認(rèn)跳轉(zhuǎn)到登錄界面。為了區(qū)分這些權(quán)限糜芳,特地的做了一個(gè)枚舉類(不用枚舉也行,用字符串?dāng)?shù)據(jù)代替也是可以的)
package com.eelinker.shiro.entity;
import java.util.HashSet;
import java.util.Set;
/**
*角色類型
*
* @author Administrator
*/
public enum Role {
//老板 所有權(quán)限
boss(permissionType.administration, permissionType.make, permissionType.check),
//員工 制作權(quán)限
staff(permissionType.make),
//檢查人員 查詢權(quán)限
checker(permissionType.check),
//顧客 使用權(quán)限
customer;
private final permissionType[] types;
Role(permissionType... types) {
this.types = types;
}
/**
* 權(quán)限類型
*/
public enum permissionType {
administration,
make,
check;
@Override
public String toString() {
return name();
}
}
/**
* 獲取用戶的權(quán)限名稱
*/
public Set<String> toSetValue() {
Set<String> permsSet = new HashSet<>();
for (permissionType type : types) {
System.out.println(type.name());
permsSet.add(type.name());
}
return permsSet;
}
}
UserService
在將用戶的信息封裝好之后魄衅,接下來(lái)就是寫Service的代碼了峭竣,因?yàn)檫@里只是演示,所以只需要展示用戶的訪問(wèn)權(quán)限就行了晃虫。靜態(tài)的List集合代替數(shù)據(jù)庫(kù)中的內(nèi)容皆撩,select代替mybatis的查詢語(yǔ)句封裝。根據(jù)用戶名查詢用戶的具體數(shù)據(jù)哲银。
import com.eelinker.shiro.entity.Role;
import com.eelinker.shiro.entity.User;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* Created System: Windows7
* Created Code by Administrator
* Created Date: 2022/1/13 16:05
*
* @author Administrator
*/
@Service
public class UserService {
private static List<User> userList = new ArrayList<User>() {
{
add(new User(1, "admin", "admin123", Role.boss));
add(new User(2, "checker", "checker123", Role.checker));
add(new User(3, "staff_zhang", "staff003", Role.staff));
add(new User(4, "staff_li", "staff004", Role.staff));
add(new User(5, "staff_wang", "staff005", Role.staff));
add(new User(6, "staff_zhao", "staff006", Role.staff));
add(new User(7, "customer7", "customer", Role.customer));
add(new User(8, "customer8", "customer", Role.customer));
add(new User(9, "customer9", "customer", Role.customer));
add(new User(10, "customer10", "customer", Role.customer));
add(new User(11, "customer11", "customer", Role.customer));
}
};
private static User select(String username) {
for (User user : userList) {
if (user.getUsername().equals(username)) {
return user;
}
}
return null;
}
}
shiro配置部分
讀取yml配置文件數(shù)據(jù)內(nèi)容
在springboot中是可以自己設(shè)置yml中的鍵值對(duì)的扛吞。這樣方便開(kāi)發(fā)者修改,調(diào)試荆责。這里是我自定義的shiro配置滥比。使用shiro作為前綴。在shiro的下一級(jí)子參數(shù)中做院,login-url這個(gè)key值寫成loginUrl也是可以的盲泛。
配置文件的名稱定位application-shiro.yml(后綴改為yaml也是可以的)
shiro:
#登錄地址url
login-url: /login.html
#未經(jīng)授權(quán)的跳轉(zhuǎn)
unauthorized-url: /unauthor.html
#登錄成功的url
success-url: /index.html
filter-class:
- anon,org.apache.shiro.web.filter.authc.AnonymousFilter
- logout,org.apache.shiro.web.filter.authc.LogoutFilter
#路徑過(guò)濾規(guī)則
filter-rule:
- /**.html,anon
- /index/**,anon
- /user/**,anon
#注銷登錄的地址,不需要開(kāi)發(fā)者寫相關(guān)的代碼或業(yè)務(wù)键耕,也能退出登錄
- /logout,logout
#需要有這個(gè)名稱的權(quán)限才能訪問(wèn)這個(gè)路徑(這里沒(méi)有提登錄寺滚,不登錄也會(huì)觸發(fā),之后跳轉(zhuǎn)到unauthorized-url)
- /select/**,perms["check"]
- /**,authc
在寫好了配置之后屈雄,這還是不能讓開(kāi)發(fā)者直接讀取村视。需要在主配置相中配置,也就是application.yml這個(gè)文件酒奶。
下面這里server.port這里是修改項(xiàng)目端口的位置
spring.profiles.active這里是配置springboot蚁孔,讀取上面配置的application-shiro.yml文件。
server:
port: 8052
spring:
profiles:
active: shiro
messages:
encoding: UTF-8
main:
allow-bean-definition-overriding: true
在配置好上面信息之后讥蟆,就可以創(chuàng)建一個(gè)實(shí)體類封裝這些數(shù)據(jù)了勒虾。代碼如下。因?yàn)榕渲弥行枰氖且唤M鍵值對(duì)而不是list集合瘸彤,所以可以用“,”隔開(kāi)兩個(gè)兩個(gè)數(shù)據(jù)前面的是路徑和面的是具體的內(nèi)容
filterClass是攔截器的類加載修然,在getFilterClassValue方法中會(huì)創(chuàng)建出這一個(gè)類,然后返回。
filterRule這是攔截路徑愕宋,前面的是路徑玻靡,后面的是允許訪問(wèn)的條件
@Component
@ConfigurationProperties(prefix = "shiro")
public class ShiroConfigReader {
private String loginUrl;
private String unauthorizedUrl;
private String successUrl;
private List<String> filterClass;
private List<String> filterRule;
// get set toString省略,在idea中可以使用alt+insert鍵創(chuàng)建
public Map<String, Filter> getFilterClassValue() throws Exception {
Map<String, Filter> map = new HashMap<>();
if (filterClass != null) {
for (String string : filterClass) {
String[] keyAndValue = string.split(",");
if (keyAndValue.length == 2) {
Class aClass = ClassLoader.getSystemClassLoader().loadClass(keyAndValue[1]);
Object object = aClass.newInstance();
if (object instanceof Filter) {
map.put(keyAndValue[0], (Filter) object);
}
}
}
if (map.size() != 0) {
return map;
}
}
throw new IllegalAccessException("The parameter is empty or the value is incorrect");
}
public Map<String, String> getFilterRuleMap() {
Map<String, String> map = new HashMap<>();
if (filterRule != null) {
for (String string : filterRule) {
String[] keyAndValue = new String[2];
int index = string.indexOf(",");
if (index != -1) {
keyAndValue[0] = string.substring(0, index);
keyAndValue[1] = string.substring(index + 1);
map.put(keyAndValue[0], keyAndValue[1]);
} else {
throw new IllegalArgumentException("The yml file parameter is incorrect");
}
}
return map;
}
throw new IllegalArgumentException("The yml file parameter is incorrect");
}
}
拿到這些配置信息之后中贝,就可以根據(jù)內(nèi)容讀取數(shù)據(jù)信息囤捻,之后的修改和添加都可以在yml中進(jìn)行。之前的上一個(gè)java文件中有@Component這個(gè)注解邻寿,所以在下面的shiro中可以使用@Autowired讀取蝎土。在shiroFilterFactoryBean讀取加載這些信息
import com.eelinker.shiro.config.reader.ShiroConfigReader;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* springBoot整合jwt實(shí)現(xiàn)認(rèn)證有三個(gè)不一樣的地方,對(duì)應(yīng)下面abc
*
* @author Administrator
*/
@Configuration
public class ShiroConfig {
@Autowired
private ShiroConfigReader reader;
@Bean
public Realm realm() {
return new MyRealm();
}
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm());
// 關(guān)閉 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() throws Exception {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager());
shiroFilter.setLoginUrl(reader.getLoginUrl());
shiroFilter.setUnauthorizedUrl(reader.getUnauthorizedUrl());
shiroFilter.setFilters(reader.getFilterClassValue());
shiroFilter.setFilterChainDefinitionMap(reader.getFilterRuleMap());
return shiroFilter;
}
}
在第一個(gè)的@Bean中里面創(chuàng)建了一個(gè)MyRealm這個(gè)類的對(duì)象绣否。這里就是用戶的授權(quán)認(rèn)證所需要用到的內(nèi)容誊涯。
doGetAuthorizationInfo方法,判斷當(dāng)前訪問(wèn)的用戶是否具有訪問(wèn)權(quán)限蒜撮,例如檢查暴构,管理制作權(quán)限。這里會(huì)查詢用戶的數(shù)據(jù)信息段磨,使用到數(shù)據(jù)庫(kù)驗(yàn)證取逾,讀取用戶信息進(jìn)行驗(yàn)證。
doGetAuthenticationInfo這個(gè)方法師在用戶調(diào)用subject.login(token)時(shí)苹支,會(huì)進(jìn)入到這里進(jìn)行權(quán)限的配置砾隅。
import com.eelinker.shiro.entity.User;
import com.eelinker.shiro.service.UserService;
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.beans.factory.annotation.Autowired;
import java.util.Set;
public class MyRealm extends AuthorizingRealm {
@Autowired
private UserService service;
/**
* 用戶身份認(rèn)證
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String username = (String) principalCollection.getPrimaryPrincipal();
User user = service.selectByUsername(username);
// 權(quán)限Set集合
Set<String> permsSet = user.getRole().toSetValue();
// 返回權(quán)限
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setStringPermissions(permsSet);
return info;
}
/**
* 用戶權(quán)限認(rèn)證(登錄)
* 這個(gè)token就是從過(guò)濾器中傳入的jwtToken
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//這里返回的是類似賬號(hào)密碼的東西,但是jwtToken都是jwt字符串沐序。還需要一個(gè)該Realm的類名
return new SimpleAuthenticationInfo(token.getPrincipal(), token.getCredentials(), "MyRealm");
}
}
但是查詢用戶所擁有的權(quán)限這里琉用,沒(méi)有userService.selectByUsername方法,所以到userService中添加一個(gè)這個(gè)方法
public User selectByUsername(String username) {
for (User user : userList) {
if (user.getUsername().equals(username)) {
return user;
}
}
return null;
}
Controller簡(jiǎn)單創(chuàng)建
在前面創(chuàng)建的頁(yè)面中有一個(gè)登錄頁(yè)面,那么現(xiàn)在就進(jìn)行登錄操作策幼。類的創(chuàng)建就省略了邑时,這里自動(dòng)注入了一個(gè)UserService。mapping中可以驗(yàn)證
@Autowired
private UserService userService;
/**
* 用戶登錄
*
* @param username
* @param password
* @return
*/
@RequestMapping("/user/login")
public String login(@RequestParam String username, @RequestParam String password) {
User user = userService.login(username, password);
if (user != null) {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
subject.login(token);
return "redirect:/index.html";
}
return "redirect:/login.html";
}
但是目前沒(méi)有寫驗(yàn)證的service代碼特姐【穑回到UserService添加如下代碼,驗(yàn)證用戶名和密碼是否正確唐含。
public User login(String username, String password) {
User user = select(username);
if (user == null) {
throw new NullPointerException("User does not exist !");
}
if (user.getPassword().equals(password)) {
return user;
} else {
System.out.println("密碼錯(cuò)誤");
}
return null;
}
從下圖中可以看到浅浮,先輸入的純域名,之后跳轉(zhuǎn)到了login.html就表名shiro的攔截頁(yè)面起了效果
好捷枯,看看登錄頁(yè)面滚秩,輸入下面數(shù)據(jù),查看是否登錄成功淮捆,成功就會(huì)進(jìn)入index.html頁(yè)面
回到代碼郁油,寫一個(gè)需要有權(quán)限才能訪問(wèn)的頁(yè)面本股,這里面的select在之前的yml文件中是已經(jīng)配置過(guò)的了。需要有查詢權(quán)限才能訪問(wèn)桐腌。代碼寫好重啟拄显,去網(wǎng)頁(yè)上訪問(wèn)
@ResponseBody
public Map<String, Object> getInfo() throws Exception {
Map<String, Object> map = new HashMap<>();
map.put("filterRule", reader.getFilterRuleMap());
map.put("filterClass", reader.getFilterClassValue());
map.put("loginUrl", reader.getLoginUrl());
map.put("unauthorizedUrl", reader.getUnauthorizedUrl());
map.put("successUrl", reader.getSuccessUrl());
return map;
}
網(wǎng)頁(yè)輸入localhost:8052/select/info,執(zhí)行訪問(wèn)案站,由于已經(jīng)重啟躬审,多以他會(huì)跳轉(zhuǎn)到登錄頁(yè)面,這時(shí)候需要重新登錄蟆盐。(如果還是在首頁(yè)面承边,那就是網(wǎng)頁(yè)緩存的問(wèn)題,清一下就好)
如果出現(xiàn)一下的狀態(tài)石挂,就是沒(méi)問(wèn)題的
如果用戶沒(méi)用登錄炒刁,或者登錄了但是沒(méi)有這個(gè)查驗(yàn)的權(quán)利呢?用戶呢誊稚?
輸入localhost:8052/logout注銷登錄,再次輸入localhost:8052/select/info嘗試罗心。
沒(méi)登錄的用戶不能訪問(wèn)里伯,會(huì)跳轉(zhuǎn)到登錄頁(yè)面,而登錄的用戶渤闷,卻沒(méi)有這個(gè)權(quán)限的疾瓮,會(huì)跳轉(zhuǎn)到如下頁(yè)面,之后再跳轉(zhuǎn)到登錄頁(yè)面