請多多留言指教
什麼是Shiro?
Apache Shiro是一個強大且易用的Java安全架構,執行身份驗證、授權、密碼和會話管理。
Shiro核心元件
Subject,SecurityManager,Realms.
1、Subject:即“目前操作使用者”。但是,在Shiro中,Subject這一概念并不僅僅指人,也可以是第三方程序、背景帳戶或其他類似事物。它僅僅意味着“目前跟軟體互動的東西”。
Subject代表了目前使用者的安全操作,SecurityManager則管理所有使用者的安全操作。
2、SecurityManager:它是Shiro架構的核心,典型的Facade模式,Shiro通過SecurityManager來管理内部元件執行個體,并通過它來提供安全管理的各種服務。
3、Realm: Realm充當了Shiro與應用安全資料間的“橋梁”或者“連接配接器”。也就是說,當對使用者執行認證(登入)和授權(通路控制)驗證時,Shiro會從應用配置的Realm中查找使用者及其權限資訊。
通俗說三者的關系:
Subject收集使用者名密碼,交給SecurityManager,SecurityManager負責排程Realms來比對使用者名密碼,如果傳回true則驗證成功,否則驗證失敗。
Shiro的特性?
Authentication(認證), Authorization(授權), Session Management(會話管理), Cryptography(加密)被 Shiro 架構的開發團隊稱之為應用安全的四大基石。
Authentication(認證):使用者身份識别,通常被稱為使用者“登入”。
Authorization(授權):通路控制。比如某個使用者是否具有某個操作的使用權限。
Session Management(會話管理):特定于使用者的會話管理。
Cryptography(加密):在對資料源使用加密算法加密的同時,保證易于使用
shiro介紹不多說了,直接上自己測試的案例代碼!!!
注:
<a> 資料庫:MySQL
<b> 相關表:使用者表,角色表,權限表,使用者角色表,角色權限表(使用者、角色、權限等資料查詢代碼省略)
<c> CommonConstant.SHIRO_ENCRYPTION_NUMBER公共常量值1024,即密碼加鹽加密次數
1、pom.xml配置
注:相關springboot依賴省略!
<!-- shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.5.2</version>
</dependency>
<!-- shiro ehcache緩存 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.5.3</version>
</dependency>
<!-- shiro-core -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.5.3</version>
</dependency>
2、shiro config配置
package com.ayiol.business.config;
import com.ayiol.business.AyiolBackendApplication;
import com.ayiol.business.constant.CommonConstant;
import com.ayiol.business.shiro.MyShiroRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.mgt.RememberMeManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
/**
* shiro config
*
* @Author: LJG
* @Date: 2020-07-18
*/
@Configuration
public class ShiroConfig {
private static final Logger logger = LoggerFactory.getLogger(AyiolBackendApplication.class);
/**
* 密碼校驗規則HashedCredentialsMatcher
* 這個類是為了對密碼進行編碼的 ,
* 防止密碼在資料庫裡明碼儲存 , 當然在登陸認證的時候 ,
* 這個類也負責對form裡輸入的密碼進行編碼
* 處理認證比對處理器:如果自定義需要實作繼承HashedCredentialsMatcher
*/
@Bean("hashedCredentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//加密方式 雜湊演算法:MD2、MD5、SHA-1、SHA-256、SHA-384、SHA-512等。
credentialsMatcher.setHashAlgorithmName("MD5");
//加密次數
credentialsMatcher.setHashIterations(CommonConstant.SHIRO_ENCRYPTION_NUMBER);
credentialsMatcher.setStoredCredentialsHexEncoded(true);
return credentialsMatcher;
}
/**
* 自定義身份認證 realm;
* <p>
* 必須寫這個類,并加上 @Bean 注解,目的是注入 MyShiroRealm,否則會影響 MyShiroRealm類 中其他類的依賴注入
*/
@Bean
public MyShiroRealm myShiroRealm() {
MyShiroRealm myShiroRealm = new MyShiroRealm();
return myShiroRealm;
}
/**
* 注入 securityManager
* 權限管理,配置主要是Realm的管理認證
*/
@Bean("securityManager")
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
return securityManager;
}
/**
* Filter工廠,設定對應的過濾條件和跳轉條件
*
* @param securityManager securityManager
* @return ShiroFilterFactoryBean
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
logger.info("======>load shiro config");
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//設定securityManager
bean.setSecurityManager(securityManager);
//設定登入頁面
bean.setLoginUrl("/login");
// //設定登入成功跳轉的頁面
// bean.setSuccessUrl("/pages/index.html");
// //設定未授權跳轉的頁面
// bean.setUnauthorizedUrl("/pages/unauthorized.html");
//定義過濾器
LinkedHashMap<String, String> filterMapper = new LinkedHashMap<>();
/**
* 放行靜态資源
*/
filterMapper.put("/css/**", "anon");
filterMapper.put("/images/**", "anon");
filterMapper.put("/js/**", "anon");
/**
* 放行公共界面
*/
filterMapper.put("/login", "anon");
filterMapper.put("/logout", "anon");
//需要登入通路的資源 , 一般将/**放在最下邊
filterMapper.put("/**", "authc");
bean.setFilterChainDefinitionMap(filterMapper);
return bean;
}
/**
* 開啟aop注解支援
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
public RememberMeManager rememberMeManager() {
CookieRememberMeManager rememberMeManager = new CookieRememberMeManager();
//注入自定義cookie(主要是設定壽命, 預設的一年太長)
SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
simpleCookie.setHttpOnly(true);
//設定RememberMe的cookie有效期
simpleCookie.setMaxAge(CommonConstant.SHIRO_COOKIE_EXPIRE_TIME);
rememberMeManager.setCookie(simpleCookie);
//手動設定對稱加密秘鑰,防止重新開機系統後系統生成新的随機秘鑰,防止導緻用戶端cookie無效
rememberMeManager.setCipherKey(Base64.decode("YV95aW9fbDIwXzIwc2VfcnZlcg=="));
return rememberMeManager;
}
@Bean
public EhCacheManager getCache() {
return new EhCacheManager();
}
}
3、MyShiroRealm自定義Beam
package com.ayiol.business.shiro;
import com.ayiol.business.AyiolBackendApplication;
import com.ayiol.business.Utils.RedisUtil;
import com.ayiol.business.Utils.SpringBeanFactoryUtil;
import com.ayiol.business.constant.CommonConstant;
import com.ayiol.business.entity.User;
import com.ayiol.business.entity.UserRole;
import com.ayiol.business.service.RoleService;
import com.ayiol.business.service.UserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* shiro realm
*
* @Author: LJG
* @Date: 2020-07-08 16:00
*/
public class MyShiroRealm extends AuthorizingRealm {
private static final Logger logger = LoggerFactory.getLogger(AyiolBackendApplication.class);
@Autowired
private UserService userService;
@Autowired
private RedisUtil redisUtil;
@Autowired
private RoleService roleService;
/**
* 授權
*
* @param principalCollection 主要的控制器
* @return Authorization
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
logger.info("=========> 進入自定義權限設定方法!");
if (null == userService) {
userService = (UserService) SpringBeanFactoryUtil.getBeanByName("userServiceImpl");
}
if (null == roleService) {
roleService = (RoleService) SpringBeanFactoryUtil.getBeanByName("roleServiceImpl");
}
// 1 擷取使用者資訊
User user = (User) principalCollection.getPrimaryPrincipal();
if (null == user) {
return null;
}
// 2 擷取使用者的角色id
List<UserRole> userRoles = userService.getAuthUserRole(null == user ? "" : user.getId());
List<String> roleIds = userRoles.stream().filter(f -> null != f.getRole()).map(ur -> ur.getRole().getId()).collect(Collectors.toList());
List<String> roleCodes = userRoles.stream().filter(f -> null != f.getRole()).map(ur -> ur.getRole().getCode()).collect(Collectors.toList());
Set<String> roleCodesSet = new HashSet<>(roleCodes);// 轉換為Set類型
// 3 擷取使用者的權限id
List<String> permissionNames = roleService.getAuthPermission(roleIds);
Set<String> permissionNamesSet = new HashSet<>(permissionNames);// 轉換為Set類型
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
// 4 設定權限
simpleAuthorizationInfo.setStringPermissions(permissionNamesSet);
// 5 設定角色
simpleAuthorizationInfo.setRoles(roleCodesSet);
return simpleAuthorizationInfo;
}
/**
* 身份認證
*
* @param token 認證token
* @return Authentication
* @throws AuthenticationException 認證異常
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
logger.info("=========> 進入自定義登入認證方法!");
//表示沒有登入
//加這一步的目的是在Post請求的時候會先進認證,然後在到請求
if (null == token.getPrincipal()) {
return null;
}
// 如果userService為空時,則手動加載userService
if (null == userService) {
userService = (UserService) SpringBeanFactoryUtil.getBeanByName("userServiceImpl");
}
// 實際項目中,這裡可以根據實際情況做緩存,如果不做,Shiro自己也是有時間間隔機制,2分鐘内不會重複執行該方法
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
String username = usernamePasswordToken.getUsername();
String pwd = String.valueOf(usernamePasswordToken.getPassword());
// 通過username從資料庫中查找 User對象
User user = userService.queryByUsername(username);
if (null == user) {
throw new UnknownAccountException(); // 使用者不存在
}
// ------------------------------- 認證方式:SimpleAuthenticationInfo加鹽 -----------------------------
// 給登入使用者名、密碼加鹽,按特定規則生成一個新的密碼;指派給usernamePasswordToken用于跟SimpleAuthenticationInfo校驗密碼是否一緻
String saltPwd = createSalting(username, pwd);
usernamePasswordToken.setPassword(saltPwd.toCharArray());
//根據使用者的情況,來建構AuthenticationInfo對象,通常使用的實作類為SimpleAuthenticationInfo
//以下資訊是從資料庫中擷取的
//1)principal:認證的實體資訊,可以是username,也可以是資料庫表對應的使用者的實體對象
Object principal = user.getUsername();
//2)credentials:密碼
Object credentials = user.getPassword();
//3)realmName:目前realm對象的name,調用父類的getName()方法即可
String realmName = getName();
//4)credentialsSalt鹽值
ByteSource credentialsSalt = ByteSource.Util.bytes(principal);//使用賬号作為鹽值
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, credentials, credentialsSalt, realmName);
return info;
}
/**
* 密碼加鹽
*
* @param username 使用者名
* @param crdentials 密碼原值
* @return 加鹽後的密碼
*/
private static String createSalting(String username, String crdentials) {
String hashAlgorithmName = "MD5";//加密方式
ByteSource salt = ByteSource.Util.bytes(username);//以使用者名為鹽值
int hashIterations = CommonConstant.SHIRO_ENCRYPTION_NUMBER;//加密次數
Object result = new SimpleHash(hashAlgorithmName, crdentials, salt, hashIterations);
return result + "";
}
}
4、LoginController.java /login登入路由
@RequestMapping(value = "/login", method = RequestMethod.POST)
public Map<String, Object> login(@RequestBody Map paramMap) {
// 擷取參數 校驗
String username = StringUtils.isEmpty(paramMap.get("username")) ? "" : paramMap.get("username").toString();
String password = StringUtils.isEmpty(paramMap.get("password")) ? "" : paramMap.get("password").toString();
if ("".equals(username)) {
return ResultUtil.badRequest("username", paramMap.get("username"), "使用者名不能為空");
}
if ("".equals(password)) {
return ResultUtil.badRequest("password", paramMap.get("password"), "密碼不能為空");
}
// //如果使用者已登入,先踢出
// ShiroSecurityHelper.kickOutUser(user.getUsername());
/**
* 使用shiro認證
*/
//1,擷取subject
Subject subject = SecurityUtils.getSubject();
//2,封裝使用者資料
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
if (!StringUtils.isEmpty(paramMap.get("rememberMe"))
&& CommonConstant.ONE_STRING.equals(paramMap.get("rememberMe").toString())) {
token.setRememberMe(true);
}
//3,執行登入方法
try {
subject.login(token);
} catch (UnknownAccountException uae) {
// 使用者名未知
return ResultUtil.failureResponse("使用者不存在!");
} catch (IncorrectCredentialsException ice) {
// 憑據不正确,例如密碼不正确
return ResultUtil.failureResponse("密碼錯誤!");
} catch (LockedAccountException lae) {
// 使用者被鎖定,例如管理者把某個使用者禁用
return ResultUtil.failureResponse("使用者被鎖定!");
} catch (ExcessiveAttemptsException eae) {
// 嘗試認證次數多餘系統指定次數
return ResultUtil.failureResponse("嘗試認證次數過多,請稍後重試!");
} catch (AuthenticationException ae) {
// 其他未指定異常
return ResultUtil.failureResponse("未知異常!");
}
// 從shiro中擷取使用者資訊
User user = (User) SecurityUtils.getSubject().getPrincipal();
user.setPassword(null);
return ResultUtil.ok(user);
}
5、LoginController.java /logout登出路由
@RequestMapping(value = "/logout", method = RequestMethod.POST)
public Map<String, Object> logout(@RequestBody Map paramMap) {
Subject subject = SecurityUtils.getSubject();
if (subject.isAuthenticated()) {
User user = (User) subject.getPrincipal();// 擷取登入使用者資訊
subject.logout();
}
return ResultUtil.ok("登出成功!");
}
6、項目為前後端分離,這裡使用的注解方式測試認證、授權
import com.alibaba.fastjson.JSON;
@RequiresRoles(value = {"admin"})
@RequiresPermissions(value = {"all", "read"}, logical = Logical.OR)
@PostMapping("")
public Map<String, Object> save(@RequestBody Map paramMap) {
if (StrUtil.isEmptyIfStr(paramMap.get("username"))) {
return ResultUtil.failureResponse("登入使用者名不能為空");
}
User exsistUser = userService.queryByUsername(paramMap.get("username").toString());
if (null != exsistUser) {
return ResultUtil.failureResponse("登入使用者名已經存在");
}
//轉換為使用者實體類
User user = JSON.parseObject(JSON.toJSONString(paramMap), User.class);
user.setId(UUID.randomUUID().toString());
user.setCreatedAt(new Date());
userService.save(user);
user.setPassword(null);
return ResultUtil.ok(user);
}
到此就完成了!有什麼問題可以留言。