Shiro
shiro是一個java的安全框架
官網地址 http://shiro.apache.org/
目錄
Shiro綜述
graph LR A1("CacheManager")-->B A2("Realms")-->B A3("UserDao")-->C A4("CredentialsMatcher")-->C A1-->C subgraph Shiro A("Subject(用戶)")-->B("SecurityManager(安全管理器)") B-->C("Realm域") end
- Subject:主體,代表了當前 “用戶”
- SecurityManager:安全管理器;即所有與安全有關的操作都會與 SecurityManager 交互;且它管理着所有 Subject;是 Shiro 的核心
- Realm:域,Shiro 從從 Realm 獲取安全數據(如用戶、角色、權限),就是說 SecurityManager 要驗證用戶身份,那么它需要從 Realm 獲取相應的用戶進行比較以確定用戶身份是否合法;也需要從 Realm 得到用戶相應的角色 / 權限進行驗證用戶是否能進行操作;可以把 Realm 看成 DataSource,即安全數據源。
Shiro 不提供維護用戶 / 權限,而是通過 Realm 讓開發人員自己注入。
參考Shiro提供的JdbcRealm中源碼的實現
//獲取用戶,其會自動綁定到當前線程
Subject subject = SecurityUtils.getSubject();
//構建待認證token
UsernamePasswordToken token = new UsernamePasswordToken("zhang", "123");
//登錄,即身份驗證
subject.login(token);
//判斷是否已經認證
subject.isAuthenticated()
//登出
subject.logout(token);
graph TB
A(Realm)-->B(CachingRealm)
B-->C(AuthenticatingRealm認證)
C-->D(AuthorizingRealm授權)
D-->E(自己實現的Realm)
D-->E1(Shiro提供的JdbcRealm)
E1-->F1(參考內部實現)
E-->F("doGetAuthorizationInfo()")
E-->G("doGetAuthenticationInfo()")
style E fill:#f96
過濾器
認證攔截器
- anon 匿名攔截器,不需要認證即可訪問,如 /static/**=anon,/login=anon
- authc 需要認證才可以訪問,如/**=authc
- user 用戶已經身份驗證 / 記住我登錄的都可;示例 /**=user
- logout 退出攔截器,如 /logout=logout
注意authc和user的區別
授權攔截器
- roles 角色授權攔截器,驗證用戶是否擁有角色;如:/admin/**=roles[admin]
- perms 權限授權攔截器,驗證用戶是否擁有所有權限;/user/**=perms["user:create"]
注解
- @RequiresPermissions 驗證權限
- @RequiresRoles 驗證角色
- @RequiresUser 驗證用戶是否登錄(包含通過記住我登錄的)
- @RequiresAuthentication 驗證是否已認證(不含通過記住我登錄的)
- @RequiresGuest 不需要認證即可訪問
//擁有ADMIN角色同時還要有sys:role:info權限
@RequiresRoles(value={"ADMIN")
@RequiresPermissions("sys:role:info")
整合Shiro
1. 配置SecurityManager
注入Realm和CacheManager(選)
@Bean("securityManager")
public org.apache.shiro.mgt.SecurityManager securityManager(ShrioRealm shrioRealm, PhoneRealm phoneRealm) {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
// //注入自定義myRealm
// defaultWebSecurityManager.setRealm(shrioRealm);
//設置多個realm,用戶名密碼登錄realm,手機號短信驗證碼登錄realm
List<Realm> realms = new ArrayList<>();
realms.add(shrioRealm);
realms.add(phoneRealm);
defaultWebSecurityManager.setRealms(realms);
return defaultWebSecurityManager;
}
2.實現Realm
注入密碼驗證器,設置是否啟用緩存
/**
*
* 自定義realm
* @author yuxf
* @version 1.0
* @date 2020/12/21 16:10
*/
public class ShrioRealm extends AuthorizingRealm {
@Autowired
TestShiroUserService userService;
/**
* 獲取授權信息
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//從數據庫取角色
Set<String> roles = userService.getRoles();
SimpleAuthorizationInfo simpleAuthorizationInfo=new SimpleAuthorizationInfo();//權限信息
simpleAuthorizationInfo.addRoles(roles);
simpleAuthorizationInfo.addStringPermission("user:create");
return simpleAuthorizationInfo;
}
/**
* 獲取認證信息
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
if(token.getPrincipal()==null)return null;
String userName=token.getPrincipal().toString();
//從數據庫查詢用戶名
String dbUser = userService.loadUserByUserName(userName);
if(dbUser==null||"".equals(dbUser)) throw new UnknownAccountException();
//密碼鹽
ByteSource salt = ByteSource.Util.bytes(userName);
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(userName, "123456", salt,getName());
return simpleAuthenticationInfo;
}
}
@Bean
public ShrioRealm shrioRealm() {
ShrioRealm shrioRealm = new ShrioRealm();
//設置密碼加密規則
shrioRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return shrioRealm;
}
/**
* 憑證匹配器
*
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:這里使用MD5算法;
hashedCredentialsMatcher.setHashIterations(2);//散列的次數,比如散列兩次,相當於 md5(md5(""));
return hashedCredentialsMatcher;
}
/**
* 注冊時需要生成密碼和密碼鹽存入數據庫
* @author yuxf
* @version 1.0
* @date 2020/12/22 17:01
*/
public class PasswordHelper {
private static String algorithmName = "md5";
private static final int hashIterations = 2;
/**
* 獲取隨機密碼鹽
* @return
*/
public static String getSalt()
{
RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
String salt = randomNumberGenerator.nextBytes().toHex();
return salt;
}
/**
* 生成密碼
* @param plainPassword 明文密碼
* @param salt 密碼鹽
* @return
*/
public static String getPassowrd(String plainPassword,String salt)
{
String newPassword = new SimpleHash(algorithmName, plainPassword, salt, hashIterations).toHex();
return newPassword;
}
}
3.配置LifecycleBeanPostProcessor
/**
* 配置LifecycleBeanPostProcessor 可以自動調用配置在Spring IOC容器中 Shiro Bean的生命周期方法
* @return
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
4.啟動注解
/**
* 配置注解生效
*
* @return
*/
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
/**
* 配置注解生效
*
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") org.apache.shiro.mgt.SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor sourceAdvisor = new AuthorizationAttributeSourceAdvisor();
sourceAdvisor.setSecurityManager(securityManager);
return sourceAdvisor;
}
5.配置ShiroFilter
ssm項目中坑
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") org.apache.shiro.mgt.SecurityManager securityManager) {
//shiro對象
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(securityManager);
bean.setLoginUrl("/shiro/login");
bean.setSuccessUrl("/shrio/index");
LinkedHashMap<String, String> linkedHashMap = new LinkedHashMap<String, String>();
//認證順序是從上往下執行。
linkedHashMap.put("/logout", "logout");//在這兒配置登出地址,不需要專門去寫控制器。
linkedHashMap.put("/shiro/phoneLogin", "anon");
linkedHashMap.put("/demo/**", "anon");
linkedHashMap.put("/static/**", "anon");
linkedHashMap.put("/shiro/anon", "anon");
linkedHashMap.put("/**", "user");//需要進行權限驗證
bean.setFilterChainDefinitionMap(linkedHashMap);
return bean;
}
SSM項目中web.xml中配置shiroFilter
<!-- shiro過慮器,DelegatingFilterProxy通過代理模式將spring容器中的bean和filter關聯起來 -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<!-- 設置true由servlet容器控制filter的生命周期 -->
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
<!-- 設置spring容器filter的bean id,如果不設置則找與filter-name一致的bean -->
<init-param>
<param-name>targetBeanName</param-name>
<param-value>shiro</param-value>
</init-param>
</filter>
緩存
https://www.cnblogs.com/nuccch/p/8044226.html
思考:為什么Shiro要設計成既可以在Realm,也可以在SecurityManager中設置緩存管理器呢?
加密
https://www.cnblogs.com/cac2020/p/13850318.html
1. 注入HashedCredentialsMatcher實現(推薦)
需要自己編寫加密幫助類生成密碼和鹽值,比較靈活
@Bean
public ShrioRealm shrioRealm() {
ShrioRealm shrioRealm = new ShrioRealm();
//設置密碼加密規則
shrioRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return shrioRealm;
}
/**
* 憑證匹配器
*
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:這里使用MD5算法;
hashedCredentialsMatcher.setHashIterations(2);//散列的次數,比如散列兩次,相當於 md5(md5(""));
return hashedCredentialsMatcher;
}
加密幫助類
/**
* 注冊時需要生成密碼和密碼鹽存入數據庫
*
* @author yuxf
* @version 1.0
* @date 2020/12/22 17:01
*/
public class PasswordHelper {
private static String algorithmName = "md5";
private static final int hashIterations = 2;
/**
* 獲取隨機密碼鹽
*
* @return
*/
public static String getSalt() {
RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
String salt = randomNumberGenerator.nextBytes().toHex();
return salt;
}
/**
* 生成密碼
*
* @param plainPassword 明文密碼
* @param salt 密碼鹽
* @return
*/
public static String getPassowrd(String plainPassword, String salt) {
String newPassword = new SimpleHash(algorithmName, plainPassword, salt, hashIterations).toHex();
return newPassword;
}
}
2. 注入PasswordMatcher實現
- Shiro提供的PasswordService 相當於 密碼幫助類,可用於生成密碼和驗證密碼
- 如果使用公鹽(
hashService.setGeneratePublicSalt(true)
),則必須設置HashFormat為Shiro1CryptFormat或不設置,默認為這個,否則無法保存鹽值導致驗證失敗,密碼加密結果如:$shiro1$MD5$3$QvLJZY8JiAJMnK9vRjlG6w==$jbNS0N/3fq2KUXufYwGwWA==
,里面包含了加密的方法類型,哈希次數,鹽值,加密結果,驗證密碼時會取出加密密碼中的鹽值來hash客戶端的密碼來驗證密碼是否正確- 鹽值保存在密碼中,無需額外存儲
@Bean
public PhoneRealm phoneRealm() {
PhoneRealm phoneRealm = new PhoneRealm();
//PasswordMatcher
PasswordMatcher passwordMatcher = new PasswordMatcher();
passwordMatcher.setPasswordService(passwordService());
phoneRealm.setCredentialsMatcher(passwordMatcher);
return phoneRealm;
}
@Bean
public PasswordService passwordService()
{
DefaultHashService hashService = new DefaultHashService();
hashService.setHashIterations(3);
hashService.setHashAlgorithmName("MD5");
hashService.setGeneratePublicSalt(true);
//設置HashService
DefaultPasswordService passwordService = new DefaultPasswordService();
passwordService.setHashService(hashService);
// passwordService.setHashFormat(new HexFormat());
return passwordService;
}
多身份Realm認證
- (推薦)自定義AuthenticationToken並重寫Realm的supports方法,來明確Real支持的Token
注意不要繼承UsernamePasswordToken
public class PhoneVcodeToken implements AuthenticationToken {
private String phone;
private String vcode;
public PhoneVcodeToken(String phone,String vcode)
{
this.phone=phone;
this.vcode=vcode;
}
@Override
public Object getPrincipal() {
return phone;
}
@Override
public Object getCredentials() {
return vcode;
}
}
Realm
public class PhoneRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String userName = token.getPrincipal().toString();
if (userName.equals("admin")) {
//123456a
return new SimpleAuthenticationInfo(userName, "$shiro1$MD5$3$j8X4VX1f6T6zGiGEFIW5yA==$ipG89XmDquh++g5xXmV1dQ==", getName());
} else {
//123456
return new SimpleAuthenticationInfo(userName, "$shiro1$MD5$3$QvLJZY8JiAJMnK9vRjlG6w==$jbNS0N/3fq2KUXufYwGwWA==", getName());
}
}
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof PhoneVcodeToken;
}
}
- 自定義AuthenticationToken並加入類型參數,重寫ModularRealmAuthenticator 在doAuthenticate()方法中根據類型來選擇Realm
/**
* @author chenzhi
* @Description: 自定義當使用多realm時管理器
* @Date:Created: in 13:41 2018/8/13
* @Modified by:
*/
public class MyModularRealmAuthenticator extends ModularRealmAuthenticator {
@Override
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) {
//先判斷Realm是否為空
assertRealmsConfigured();
//強轉為自定義的Token
MyUsernamePasswordToken myUsernamePasswordToken = (MyUsernamePasswordToken) authenticationToken;
//拿到登錄類型
String loginType = myUsernamePasswordToken.getLoginType();
//拿到所有Realm集合
Collection<Realm> realms = getRealms();
List<Realm> myrealms = new ArrayList<>();
//遍歷每個realm 根據loginType將對應的Reaml加入到myrealms
for (Realm realm : realms) {
//拿到Realm的類名 ,所以在定義Realm時,類名要唯一標識並且包含枚舉中loginType某一個Type
//注意:一個Realm的類名不能包含有兩個不同的loginType
if (realm.getName().contains(loginType)) {
myrealms.add(realm);
}
}
//判斷是單Reaml還是多Realm
if (myrealms.size() == 1) {
return doSingleRealmAuthentication(myrealms.iterator().next(), myUsernamePasswordToken);
} else {
return doMultiRealmAuthentication(myrealms, myUsernamePasswordToken);
}
}
}
認證流程
token=new UsernamePasswordToken(userName,password)
graph TB subgraph Suject A1("Subject")--"subject = SecurityUtils.getSubject();"-->C1("token") B1(token)-->C1("subject.login(token)") end subgraph SecurityManager A2("securityManager.login(token)") B2("onSuccessfulLogin(token, info, loggedIn)") end subgraph ModularRealmAuthenticator A3("authenticate(token)") end subgraph Realm A4("getAuthenticationInfo(token)")--獲取認證信息-->B4("doGetAuthenticationInfo(token)") B4--傳入認證信息並驗證密碼-->C4("assertCredentialsMatch(token,info)") end subgraph CredentialsMatcher A5("doCredentialsMatch(token,info)") end C1-->A2 A2--this.authenticator-->A3 A3-->B2 A3--"this.getRealms()"-->A4 C4--"getCredentialsMatcher()"-->A5