一、shiro入門
兩大框架對比:安全框架Shiro和SpringSecurity的比較
了解shiro
什么是Shiro
- Apache Shiro是一個Java的安全(權限)框架。|
- Shiro可以完成,認證,授權,加密,會話管理,Web集成,緩存等。
shiro的功能
Authentication:身份認證、登錄,驗證用戶是不是擁有相應的身份;
Authorization:授權,即權限驗證,驗證某個已認證的用戶是否擁有某個權限,即判斷用戶能否進行什么操作,如:驗證某個用戶是否擁有某個角色,或者細粒度的驗證某個用戶對某個資源是否具有某個權限!
Session Manager:會話管理,即用戶登錄后就是第一次會話,在沒有退出之前,它的所有信息都在會話中;會話可以是普通的JavaSE環境,也可以是Web環境;
Cryptography:加密,保護數據的安全性,如密碼加密存儲到數據庫中,而不是明文存儲;
# ------------------------------------------------------------------------------------
Web Support: Web支持,可以非常容易的集成到Web環境;
Caching:緩存,比如用戶登錄后,其用戶信息,擁有的角色、權限不必每次去查,這樣可以提高效率
Concurrency: Shiro支持多線程應用的並發驗證,即,如在一個線程中開啟另一個線程,能把權限自動的傳播過去
Testing:提供測試支持;
Run As:允許一個用戶假裝為另一個用戶(如果他們允許)的身份進行訪問;
Remember Me:記住我,這個是非常常見的功能,即一次登錄后,下次再來的話不用登錄了
優點
- 簡單的身份認證, 支持多種數據源
- 對角色的簡單的授權, 支持細粒度的授權(方法級) c、支持一級緩存,以提升應用程序的性能
- 適用於 Web 以及非 Web 的環境e、非常簡單的加密 API
- 不跟任何的框架或者容器捆綁, 可以獨立運行
Shiro的架構
功能 | 描述 |
---|---|
Subject | 用戶、主體 |
Securitymanager | 管理所有用戶 |
Authorizer | 認證器、是一個接口 |
Authorizer | 授權器、判斷操作權限 |
Realm | 連接數據 |
注意:Shiro 不會去維護用戶、_維護權限;這些需要我們自己去設計/提供:然后通過相應的接口灃入給Sniro即可
依賴與配置
- 依賴
<!-- shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.7.1</version>
</dependency>
- 日志配置(log4j)
#Default Shiro Logging
1og4j.1ogger.org.apache.shiro=INFO
#Disable verbose logging
log4j.logger.org.apache.shiro.util.ThreadContext=WARN
1og4j.1ogger.org.apache.shiro.cache.ehcache.EhCache=WARN
-
shiro.ini
ini文件說明
- [mian] : 定義全局變量
- 內置securityManger對象
- 操作內置對象時,在[main]里面寫東西
[main]
securityManger.屬性=值
# --
user=com.qd.user
securityManger.對象屬性=$user
- [users] : 定義用戶名與密碼
[users]
# 定義用戶名為qd666
qd666 = 123456
# 定義用戶名為qd666 同時具有admin、role1的角色
qd666 = 123456, admin,role1
- [roles] : 定義角色
[roles]
role1=權限名1,權限名2
admin=權限名3,權限名4
# --- 用戶表的查詢,添加權限
role1=user:query,user:add
- [urls] : 定義哪些內置urls生效.在web應用時使用
[urls]
# url地址=內置filter或自定義filter
# 訪問時出現/login的url必須要去認支持authc對應的filter
/login=authc
# 任意的url都不需要進行認證等功能
/**=anon
# 所有的內容都必須保證用戶已經登錄
/**=user
# url abc訪問時必須保證用戶具有role1和role2的角色
/abc=roles["role1,role2"]
Realm結構
二、功能實現
配置實現認證
-
身份驗證
在shiro 中,用戶需要提供principals(身份)和credentials(證明)給shiro,從而應用能驗證用戶身份:
-
principals
身份,即主體的標識屬性,可以是任何東西,如用戶名、郵箱等,唯一即可。一個主體可以有多個principals,但只有一個Primary principals,一般是用戶名/密碼/手機號。
-
credentials
證明/憑正,即只有主體知道的安全值,如密碼/數字證書等最常見的principals和credentials組合就是用戶名/密碼了。
認證流程
@Slf4j
public class Certification {
//創建安全管理器工廠
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//使用工廠創建安全管理器
SecurityManager securityManager = factory.getInstance();
//把當前的安全管理器綁定到當前的線程
SecurityUtils.setSecurityManager(securityManager);
//獲取當前的用戶對象
Subject currentUser = SecurityUtils.getSubject();
//通過當前對象拿到Session
// Session session = currentUser.getSession();
// session.setAttribute("someKey","aValue");
// String value = (String) session.getAttribute("someKey");
// 判斷用戶是否認證通過
if(!currentUser.isAuthenticated()) {
//封裝用戶名與密碼
AuthenticationToken token = new UsernamePasswordToken("lonestarr", "vespa");
//設置記住我
token.setRememberMe(true);
try {
//進行認證
currentUser.login(token); //執行了登錄
} catch (LockedAccountException lae) { //用戶鎖定
log.error(token.getPrincipal() + " is locked");
} catch (AuthenticationException e) {
log.error("用戶名或密碼不正確");
}
}
}
配置實現授權
授權,也叫訪問控制,即在應用中控利誰能訪問哪些資源―如訪問頁面/編輯數據/頁面操作等)。在授權中需了解的幾個關鍵對象:主體(Subject)、資源(Resource)、權限(Permission)角色(Role)。
對象 | 描述 |
---|---|
主體 | 訪問應用的用戶 |
資源 | 某些頁面、數據 |
權限 | 判斷用戶有無操作的權力 |
角色 | 權限的集合 |
授權流程
//接認證
//打印其識別主體
log.info("User ["+currentUser.getPrincipal()+"] logged in successfully.");
//角色判斷 統一返回布爾值
currentUser.hasRole("role1");
List<String> list= Arrays.asList("role1","role2");
//判斷當前用戶是否擁有list集合中的所有角色
currentUser.hasAllRoles(list);
//--------------------------------------------------------
//權限判斷
currentUser.isPermitted("user:query");
//判斷是否擁有全部的權限
currentUser.isPermitted("user:query","user:add");
//--------------------------------------------------------
//注銷
currentUser.logout();
//結束
System.exit(0);
認證與授權中主要的方法
//獲取當前的用戶對象
Subject currentUser = SecurityUtils.getSubject();
//通過當前對象拿到Session
Session session = currentUser.getSession();
//判斷用戶是否被認證
currentUser.isAuthenticated()
//得到認證
currentUser.getPrincipal()
//判斷是否有此角色
currentUser.hasRole("schwartz")
//判斷是否有此權限
currentUser.isPermitted("lightsaber:wield")
//注銷
currentUser.logout();
注意: Shiro默認使用自帶的IniRealm,IniRealm從ini配置文件中讀取用戶的信息,但是開發中我們需要從數據庫中獲取數據,所以我們需要自定義Realm!
自定義Realm實現認證
需繼承的接口
接口 | 描述 |
---|---|
CachingRealm | 負責緩存處理 |
AuthentictionRealm | 負責認證 |
AuthorizingRealm | 負責授權 |
通常自定義的realm繼承AuthonizingRealm
- shiro配置類
固定步驟
@Configuration
public class ShiroConfig {
//1.Realm 資源 自定義userRealm對象
@Bean
public Realm userRealm() { return new UserRealm(); }
//2.securityManager 執行流程控制
@Bean("securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(UserRealm userRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//關聯Realm對象
securityManager.setRealm(userRealm);
return securityManager;
}
//3.ShiroFilterFactoryBean 請求過濾器
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//設置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
//添加過濾器 設置默認請求等操作
return bean;
}
}
在ShiroConfig 添加過濾器
map中的key值是ant路徑
** 代表多級路徑
* 代表單極路徑
? 代表一個字符
# -----value是默認過濾器-------------
anon : 無需認證就可以訪問
authc : 必須認證了才能訪問
user : 必須擁有 記住我 功能才能用
perms[] : 擁有對某個資源的權限才能訪問
role : 擁有某個角色權限才能訪問
- *Realm類 繼承
AuthorizingRealm
public class UserRealm extends AuthorizingRealm {
//授權方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("執行了授權方法");
//....
return null;
}
//認證方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("執行了認證方法");
//......
return null;
}
}
自定義Realm實現授權
- 對某些頁面上的屬性/組件進行授權
可以使用session解決
在登錄時將currentUser.getPricipal()放入session中
將currentUser.getPricipal()來自於MyRealm中doGetAuthenticationinfo認證方法返回的SimpleAuthenticationInfo對象的第一個屬性。前台獲取session判斷即可
- 控制后台資源路徑的訪問權限
- 先訪問需要授權的頁面
//硬編碼方式,代碼冗余
@RequestMapping("/xxx")
@ResponseBody
public String unauthorized() {
Subject subject = SecurityUtils.getSubject();
if (subject.isPermitted("vip1")) {
return "ok";
} else {
return "未經授權無法訪問此頁面";
}
}
//-----方法2
//設置未授權的請求(頁面) 這種方式需要添加大量的過濾器配置
bean.setUnauthorizedUrl("/xxx");
//-----方法3
//注解 如下
//硬編碼方式,代碼冗余
@RequiresPermissions("vip1")
@RequestMapping("/xxx")
@ResponseBody
public String unauthorized() {
return "ok";
}
注解 | 描述 |
---|---|
@RequiresAuthentication | 需要完成用戶登錄 |
@RequiresGuest | 未登錄用戶可以訪問,登錄用就不能訪問。 |
@RequiresPermissions | 需要有對應資源權限 |
@RequiresRoles | 需要有對應的角色 |
@RequiresUser | 需要完成用戶登錄並且完成了記住我功能。 |
tips : 當沒有權限訪問時,就會拋出500異常 AuthorizationException
統一處理
模擬登錄
登錄請求 建議定義全局異常處理
@RequestMapping("/login")
public String login(String username, String password) {
//獲取當前的認證
Subject subject = SecurityUtils.getSubject();
//封裝用戶的登錄數據
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
//執行登錄方法 固定代碼 若無異常就成功
try {
subject.login(token);
return "index";
} catch (AuthenticationException e) { //用戶名不存在
model.addAttribute("msg", "用戶名或者密碼錯誤");
return "login";
}
userRealm
//@Configuration
public class UserRealm extends AuthorizingRealm {
//授權方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("執行了授權方法");
//獲取當前用戶
Subject subject = SecurityUtils.getSubject();
User currentUser = (User) subject.getPrincipal();
//定義info,封裝這個對象所有的角色和權限
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//判斷登錄者是否為超級管理員 權限為所有
if ("超級管理員標志".equals(currentUser.getPermId())){
//1.獲取角色與權限列表
//2.綁定需要角色與權限
info.addRole("");
info.addStringPermission("");
}else {
//管理員
info.addRole("admin");
info.addStringPermission("*.*");
}
return info;
}
//認證方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("執行了認證方法");
//獲取當前用戶
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
//查詢真實數據庫
// User user = userService.queryUserByName(userToken.getUsername());
//若查詢結果為空,會拋出異常
if (user == null) {
return null;
}
//密碼認證,shiro幫我們自動匹配 ,之后就完成認證
return new SimpleAuthenticationInfo(user, user.getUserPwd(), ""); //資源,密碼
}
}
//設置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
Map<String, String> filterMap = new LinkedHashMap<>();
//添加過濾器
filterMap.put("/user/addUser", "perms[user:add]");
filterMap.put("/user/*", "authc");
bean.setFilterChainDefinitionMap(filterMap);
//設置登錄的請求(頁面) 否則會報錯
bean.setLoginUrl("/login");
//設置未授權的請求(頁面)
bean.setUnauthorizedUrl("/xxx");
return bean;
tips: 越詳細的規則放入前面哦!
退出登錄
//----方法1----請求
Subject subject = SecurityUtils.getSubject();
subject.logout();
//----方法2----過濾器
filterMap.put("/user/logout", "logout");
相關文章: Shiro 整合 SpringBoot
三、其他功能
密碼加密
shiro會獲得一個credentialsMatcher對象,來對密碼進行比對。想要用MD5方式進行加密:Md5CredentialsMatcher已經過期,要使用``HashedCredentialsMatcher`並設定算法名。
- 在``securityManager`設置CredentialsMatcher
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
//設置加密算法
matcher.setHashAlgorithmName("MD5");
//設置加密次數
matcher.setHashIterations(2);
userRealm.setCredentialsMatcher(matcher);
- 加鹽 改變構造方法
return new SimpleAuthenticationInfo(user, ByteSource.Util.bytes(user.getUserPwd()), ByteSource.Util.bytes("12345"),"");
- 匹配
public class PasswordEncoder {
public static String encoder(String password) {
SimpleHash simpleHash = new SimpleHash("MD5", ByteSource.Util.bytes(password), ByteSource.Util.bytes("12345"), 2);
return simpleHash.toString();
}
}
多Realm認證
舉個栗子:在登錄微信時,我們可以選擇使用手機號、也可以選擇微信號進行登錄,那么這種時怎么實現的呢?
-
在user表與實體中加入手機號、賬號字段
-
新建
MobileRealm
@Configuration
public class MobileRealm extends AuthenticatingRealm {
@Autowired
UserService userService;
//認證
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("執行了認證方法");
//獲取當前用戶
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
//查詢真實數據庫
User user = userService.queryUserByName(userToken.getUsername());
//若查詢結果為空,會拋出異常
if (user == null) {
return null;
}
//密碼認證,shiro幫我們自動匹配 ,之后就完成認證
return new SimpleAuthenticationInfo(user, user.getUserPwd(),"");
}
}
- 設置
securityManager
//關聯對象
securityManager.setRealms(Arrays.asList(userRealm,mobileRealm));
此時已經可以實現通過手機號也能登錄賬號 在多Realml的認證策略中,AuthenticationStrategy接口有三個實現類,了解
Remember me
- 一般前端會在頁面提供記住我選項
- 實現
if (subject.isAuthenticated() && subject.isRemembered()) {
subject.login(token);
token.setRememberMe(true);
}
-
記住我功能默認對應了``user` 過濾器
-
設置
securityManager
CookieRememberMeManager rememberMeManager = new CookieRememberMeManager();
SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
//防止xss讀取cookie
simpleCookie.setHttpOnly(true);
//記住我cookie生效時間30天 ,單位秒
simpleCookie.setMaxAge(2592000);
rememberMeManager.setCookie(simpleCookie);
securityManager.setRememberMeManager(rememberMeManager);
在微服務與分布式中 涉及到cookie共享問題 等等等頭大...
認證緩存
我們每次登錄時可以先從緩存中獲取,如果沒有在從數據庫中查詢,提高性能
- 在
securityManager
開啟緩存
//緩存
AbstractCacheManager cacheManager = new MemoryConstrainedCacheManager();
securityManager.setCacheManager(cacheManager);
記住我,會話管理以及認證緩存,都可以通過擴展對應的manager接口的方式,實現自己的靈活擴展,比如將信息共享到redis。
結合Thylemeaf
- 導入依賴
<!--導入shiro與Thymeleaf整合包-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
- 配置ShiroConfig
//整合ShiroDialect:用來整合shiro與thymeleaf
@Bean
public ShiroDialect getShiroDialect() {
return new ShiroDialect();
}
- 具體使用 : shiro springsecurity thymeleaf的標簽使用和命名空間
- 注解講解:注解使用
四、整合JWT
至此,終於到了真正想寫的地方,在實際的開發中,接口設計通常要考慮到安全與權限設計,JWT的token機制也不會占用服務器內存、利用JWT也能實現單點登錄等等功能。下面簡單整合兩者的使用吧
JSON Web Token(JWT)是一個非常輕巧的規范。這個規范允許我們使用 JWT 在用戶和服務器之間傳遞安全可靠的信息。我們利用一定的編碼生成 Token,並在 Token 中加入一些非敏感信息,將其傳遞。
當用戶登陸成功將頒發給用戶一個token,token由jwt制作。往后token過期之前,用戶訪問的憑借都是token。如果token過期或者被篡改,則要求用戶重新登陸。在token有效時,用戶訪問需要權限的接口都要經過shiro的兩個核心注解RequiresRoles以及RequiresPermissions的驗證。
- 導入依賴
<!--配置shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.7.1</version>
</dependency>
<!--配置JWT-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
- JWTUtil
生成token與校驗token 指定 token 過期時間EXPIRE_TIME
和簽名密鑰SECRET
public class JwtUtil {
//密鑰
private static final String SECRET = "!Q464rwr(&*)6%$#^%&JNJ46da";
/**
* 生成token
* @param map:要加密的信息
* @return 令牌
*/
public static String sign(Map<String, String> map) {
//設置過期時間 默認3天
Calendar instance = Calendar.getInstance();
instance.add(Calendar.DATE, 3);
JWTCreator.Builder builder = JWT.create();
//payload
map.forEach((k, v) -> {
builder.withClaim(k, v);
});
//指定令牌過期時間與加密
String token = builder.withExpiresAt(instance.getTime())
.sign(Algorithm.HMAC256(SECRET));
return token;
}
/**
* 驗證token合法性/獲取token全部信息
* @param token
* @return verify
*/
public static DecodedJWT verify(String token) {
return JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token);
}
/**
* 獲取token單個信息
* @param token
* @param s:key
* @return value
*/
public static String getInfo(String token, String s) {
DecodedJWT verify = verify(token);
return verify.getClaim(s).asString();
}
}
- JWTFilter
在上面,我們使用的是 shiro 默認的權限攔截 Filter,而因為 JWT 的整合,我們需要自定義自己的過濾器 JWTFilter
public class JWTFilter extends BasicHttpAuthenticationFilter {
private Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* 如果帶有 token,則對 token 進行檢查,否則直接通過
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnauthorizedException {
//判斷請求的請求頭是否帶上 "Token"
if (isLoginAttempt(request, response)) {
//如果存在,則進入 executeLogin 方法執行登入,檢查 token 是否正確
try {
executeLogin(request, response);
return true;
} catch (Exception e) {
System.out.println("token 錯誤");
//token 錯誤 非法訪問
throw new RuntimeException("Unauthorized access!");
// responseError(response, e.getMessage());
}
}
//如果請求頭不存在 Token,則可能是執行登陸操作或者是游客狀態訪問,無需檢查 token,直接返回 true
return true;
}
/**
* 判斷用戶是否想要登入。
* 檢測 header 里面是否包含 Token 字段
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String token = req.getHeader("Authorization");
return token != null;
}
/**
* 執行登陸操作
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader("Authorization");
JWTToken jwtToken = new JWTToken(token);
// 提交給realm進行登入,如果錯誤他會拋出異常並被捕獲
getSubject(request, response).login(jwtToken);
// 如果沒有拋出異常則代表登入成功,返回true
return true;
}
/**
* 對跨域提供支持
*/
@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"));
// 跨域時會首先發送一個option請求,這里我們給option請求直接返回正常狀態
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
/**
* 將非法請求跳轉到 /unauthorized/**
*/
private void responseError(ServletResponse response, String message) {
try {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
//設置編碼,否則中文字符在重定向時會變為空字符串
message = URLEncoder.encode(message, "UTF-8");
httpServletResponse.sendRedirect("/unauthorized/" + message);
} catch (IOException e) {
logger.error(e.getMessage());
}
}
}
- JWTToken
實現 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;
}
}
- ShiroConfig
設置好自定義的Filter,讓請求通過過濾器
@Configuration
public class ShiroConfig {
/**
* 先走 filter ,然后 filter 如果檢測到請求頭存在 token,則用 token 去 login,走 Realm 去驗證
*/
@Bean
public ShiroFilterFactoryBean factory(SecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
// 添加自己的過濾器並且取名為jwt
Map<String, Filter> filterMap = new LinkedHashMap<>();
//設置我們自定義的JWT過濾器
filterMap.put("jwt", new JWTFilter());
factoryBean.setFilters(filterMap);
factoryBean.setSecurityManager(securityManager);
// 設置無權限時跳轉的 url;
factoryBean.setUnauthorizedUrl("/unauthorized/無權限");
Map<String, String> filterRuleMap = new HashMap<>();
// 所有請求通過我們自己的JWT Filter
filterRuleMap.put("/**", "jwt");
// 訪問 /unauthorized/** 不通過JWTFilter
filterRuleMap.put("/unauthorized/**", "anon");
factoryBean.setFilterChainDefinitionMap(filterRuleMap);
return factoryBean;
}
/**
* 注入 securityManager
*/
@Bean
public SecurityManager securityManager(MyRealm myRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 設置自定義 realm.
securityManager.setRealm(myRealm);
//關閉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();
// 強制使用cglib,防止重復代理和可能引起代理出錯的問題
// 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();
}
}
- Realm類
自定義我們的Realm 實現認證與授權
@Component
public class MyRealm extends AuthorizingRealm {
private final UserMapper userMapper;
@Autowired
public CustomRealm(UserMapper userMapper) {
this.userMapper = userMapper;
}
/**
* 必須重寫此方法,不然會報錯
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}
/**
* 默認使用此方法進行用戶名正確與否驗證,錯誤拋出異常即可。
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("————身份認證方法————");
String token = (String) authenticationToken.getCredentials();
// 解密獲得username,用於和數據庫進行對比
String username = JWTUtil.getInfo(token,"username");
if (username == null) {
throw new AuthenticationException("token認證失敗!");
}
User user = userMapper.queryUserByName(username);
if (user.getPassWord() == null) {
throw new AuthenticationException("該用戶不存在!");
}
return new SimpleAuthenticationInfo(token, token, "MyRealm");
}
/**
* 只有當需要檢測用戶權限的時候才會調用此方法,例如checkRole,checkPermission之類的
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("————權限認證————");
String token= principals.toString();
String username = JWTUtil.getInfo(token,"username");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//獲得該用戶角色
User user = userMapper.queryUserByName(username);
//每個角色擁有默認的權限
int perm = user.getPerm();
//每個用戶可以設置新的權限
// String permission = userMapper.getPermission(username);
Set<String> roleSet = new HashSet<>();
Set<String> permissionSet = new HashSet<>();
//需要將 role, permission 封裝到 Set 作為 info.setRoles(), info.setStringPermissions() 的參數
roleSet.add(perm + "");
// permissionSet.add(rolePermission);
// permissionSet.add(permission);
//設置該用戶擁有的角色和權限
info.setRoles(roleSet);
info.setStringPermissions(permissionSet);
return info;
}
}
- Controller
@PostMapping("/login")
public ResultMap login(@RequestParam("username") String account,
@RequestParam("password") String password) {
User user = userMapper.queryUserByName(account);
if (user == null || !user.getPassWord().equals(password)) {
throw new RuntimeException("用戶名或密碼錯誤");
}
Map<String, String> map = new HashMap<>();
map.put("username", user.getAccount());
return R.ok(JwtUtil.sign(map), "登錄成功");
}
/**
* 擁有 1、2、3 角色的用戶可以訪問下面的頁面
*/
@GetMapping("/getMessage")
@RequiresRoles(logical = Logical.OR, value = {"1", "2", "3"})
public R getMessage() {
return R.ok("", "成功獲得信息");
}
運行結果:
不帶token訪問的情況下:
加上的情況:
The End~~