Shiro认证详解
Shiro
shiro是一个java的安全框架
官网地址 //shiro.apache.org/
Shiro综述
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>
缓存
//www.cnblogs.com/nuccch/p/8044226.html
思考:为什么Shiro要设计成既可以在Realm,也可以在SecurityManager中设置缓存管理器呢?
加密
//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)
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