最近项目用的shiro权限,然后 涉及到2种登录方式,第一种是基础的账号密码登陆,这是shiro提供的基础功能。第二种是手机短信验证码登陆。我具体讲讲第二种我的思路是用阿里云发送短信验证码的时候把code存到session中,key就是手机号,然后当用户用手机号登陆的时候我在shiro验证之前 验证用户提交的code 和 我存在session中的code是不是相同,下面详细写写我在这个项目中怎么做的。
首先是ShiroConfig
package com.mytest.configuation;
import com.mytest.filter.ShiroLoginFilter;
import com.mytest.shiro.MyCustomModularRealmAuthenticator;
import com.mytest.shiro.IdCardAndPasswordShiroRealm;
import com.mytest.shiro.PhoneAndVerificationCodeShiroRealm;
import com.mytest.shiro.UserShiroRealm;
import org.apache.shiro.authc.AbstractAuthenticator;
import org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import javax.servlet.Filter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Configuration
public class ShiroConfig {
@Bean("lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 账号密码登陆验证方式
* @return 自定义的账号密码验证Realm
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public UserShiroRealm userShiroRealm(){
return new UserShiroRealm();
}
/**
* 手机短信验证码登陆
* @return 自定义的手机短信验证Realm
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public PhoneAndVerificationCodeShiroRealm phoneShiroRealm(){
return new PhoneAndVerificationCodeShiroRealm();
}
//这里把 前面两个bean 传入到 manager中
@Bean
public SecurityManager securityManager(UserShiroRealm userShiroRealm, PhoneAndVerificationCodeShiroRealm phoneShiroRealm, AbstractAuthenticator abstractAuthenticator){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
List<Realm> realms = new ArrayList<>();
realms.add(userShiroRealm);
realms.add(phoneShiroRealm);
defaultWebSecurityManager.setRealms(realms);
defaultWebSecurityManager.setAuthenticator(abstractAuthenticator);
return defaultWebSecurityManager;
}
/**
* 认证器 把我们的自定义验证加入到认证器中
*/
@Bean
public AbstractAuthenticator abstractAuthenticator(UserShiroRealm userRealm, PhoneAndVerificationCodeShiroRealm phoneShiroRealm){
// 自定义模块化认证器,用于解决多realm抛出异常问题
//开始没用自定义异常问题,发现不管是账号密码错误还是什么错误
//shiro只会抛出一个AuthenticationException异常
ModularRealmAuthenticator authenticator = new MyCustomModularRealmAuthenticator();
// 认证策略:AtLeastOneSuccessfulStrategy(默认),AllSuccessfulStrategy,FirstSuccessfulStrategy
authenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
// 加入realms
List<Realm> realms = new ArrayList<>();
realms.add(userRealm);
realms.add(phoneShiroRealm);
authenticator.setRealms(realms);
return authenticator;
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//添加Shiro内置过滤器,常用的过滤器:
// anon:无需认证;authc:必须认证;user:如果使用rememberMe的功能可以直接访问;
// perms:必须得到资源权限;roles:必须得到角色权限
Map<String,String> filterMap = new LinkedHashMap<>();
filterMap.put("/logout.do","logout");
filterMap.put("/menu.do", "authc");
filterMap.put("/index.do","anon");
filterMap.put("/org/**","anon");
filterMap.put("/person/**","anon");
filterMap.put("/resource/**","anon");
filterMap.put("/**","anon");
filterMap.put("/**","authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}
/**
* 加入shiro注解
* @param
* @return
*/
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
1. 账号密码登陆
这种就相当于默认的shiro处理。
使用的是默认的 UsernamePasswordToken
只用自定义AuthorizingRealm
UserShiroRealm具体代码:
package com.mytest.shiro;
import com.mytest.entity.Person;
import com.mytest.repository.PersonRepository;
import com.mytest.service.PersonService;
import com.mytest.service.ResourceService;
import com.mytest.util.Constant;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
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.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Set;
public class UserShiroRealm extends AuthorizingRealm {
@Autowired
private ResourceService resourceService;
@Autowired
private PersonService personService;
@Autowired
private PersonRepository personRepository;
/**
* 执行授权
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行授权");
Person person = (Person) principalCollection.getPrimaryPrincipal();
Set<String> permissionSet = resourceService.getResourceSetByPersonId(person.getPersonId());
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addStringPermissions(permissionSet);
return simpleAuthorizationInfo;
}
/**
* 执行认证
*
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行认证");
String userName = authenticationToken.getPrincipal().toString();
SimpleAuthenticationInfo info=null;
Person user = personService.getLoginPerson(userName);
if (user == null) {
throw new UnknownAccountException();
}
//密码加密的盐值
ByteSource credentialsSalt = ByteSource.Util.bytes(user.getUsername());
info = new SimpleAuthenticationInfo(user, user.getPassword(), credentialsSalt, getName());
}
return info;
}
/**
* 自定义加密模式 MD5
*
* @param credentialsMatcher
*/
@Override
public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
HashedCredentialsMatcher MD5CredentialsMatcher = new HashedCredentialsMatcher();
MD5CredentialsMatcher.setHashAlgorithmName("MD5");
MD5CredentialsMatcher.setHashIterations(10);
super.setCredentialsMatcher(MD5CredentialsMatcher);
}
/**
* 用来判断是否使用当前的 realm
* @param var1 传入的token
* @return true就使用,false就不使用
*/
@Override
public boolean supports(AuthenticationToken var1){
return var1 instanceof UsernamePasswordToken;
}
}
账号密码登陆的 算是搞定了。因为是shiro提供的基础功能所以这一种登陆方式很简单。
2.手机短信验证码登陆
首先自定义PhoneAndVerificationCodeToken,因为是手机验证码登陆,没有密码 所以不能用shiro自带的 UsernamePasswordToken
package com.mytest.shiro;
import org.apache.shiro.authc.HostAuthenticationToken;
import org.apache.shiro.authc.RememberMeAuthenticationToken;
public class PhoneAndVerificationCodeToken implements HostAuthenticationToken, RememberMeAuthenticationToken {
private String phone;
private boolean rememberMe;
private String host;
public PhoneAndVerificationCodeToken(String phone){
this(phone,false,null);
}
public PhoneAndVerificationCodeToken(String phone,boolean rememberMe){
this(phone,rememberMe,null);
}
public PhoneAndVerificationCodeToken(String phone,boolean rememberMe,String host){
this.phone=phone;
this.rememberMe=rememberMe;
this.host=host;
}
@Override
public String getHost() {
return host;
}
@Override
public boolean isRememberMe() {
return rememberMe;
}
@Override
public Object getPrincipal() {
return phone;
}
@Override
public Object getCredentials() {
return phone;
}
}
然后自定义处理PhoneAndVerificationCodeToken的Realm
package com.mytest.shiro;
import com.mytest.entity.Person;
import com.mytest.repository.PersonRepository;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
public class PhoneAndVerificationCodeShiroRealm extends AuthorizingRealm {
@Autowired
private PersonRepository personRepository;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
PhoneAndVerificationCodeToken phoneToken=(PhoneAndVerificationCodeToken) token;
String phone = (String) phoneToken.getPrincipal();
Person user = personRepository.findByPhone(phone);
//因为没有密码,并且验证码在之前就验证了 所以我这里没有验证密码
if(user == null){
throw new UnknownAccountException();
}
return new SimpleAuthenticationInfo(user,phone,getName());
}
/**
* 用来判断是否使用当前的 realm
* @param var1 传入的token
* @return true就使用,false就不使用
*/
@Override
public boolean supports(AuthenticationToken var1) {
return var1 instanceof PhoneAndVerificationCodeToken;
}
}
最后最重要的是 要自定义模块化认证器,这也是我卡了很久的问题,之前一直没加这个,然后发现一直跟我抛AuthenticationException异常。
package com.mytest.shiro;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.pam.AuthenticationStrategy;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.realm.Realm;
import java.util.Collection;
import java.util.Iterator;
public class MyCustomModularRealmAuthenticator extends ModularRealmAuthenticator{
@Override
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
AuthenticationStrategy authenticationStrategy = this.getAuthenticationStrategy();
AuthenticationInfo authenticationInfo = authenticationStrategy.beforeAllAttempts(realms,token);
Iterator var5 = realms.iterator();
while(var5.hasNext()) {
Realm realm = (Realm)var5.next();
authenticationInfo = authenticationStrategy.beforeAttempt(realm, token, authenticationInfo);
if (realm.supports(token)) {
AuthenticationInfo info = null;
Throwable t = null;
info = realm.getAuthenticationInfo(token);
authenticationInfo = authenticationStrategy.afterAttempt(realm, token, info, authenticationInfo, t);
}
}
authenticationInfo = authenticationStrategy.afterAllAttempts(token, authenticationInfo);
return authenticationInfo;
}
}
OK,基本完成了。剩下的就是在 你的登录 controller中 不同的 登录方式 用不同的 token 然后
SecurityUtils.getSubject().logon(token)。就好了
emmmmm,shiro还有可能会遇到前后端分离的问题,光有CorsFilter还不够, 不过这篇博客不是讲这个 我就不写拉。