目录
SpringBoot整合shiro实现多用户表多Realm统一登录
前言: 一个需求 有学生和老师两个用户表 用springboot和shiro框架实现统一登录 即根据不用用户登录去查询自己的数据库
借鉴:https://blog.csdn.net/visket2008/article/details/78539334
一般的shiro登录就是通过前端传来用户的账号和密码 通过Controller里创建UsernamePasswordToken对象,然后绑定上前端访问过来的账号密码,之后由Subject.login(UsernamePasswordToken)完成登录,自己实现AuthorizingRealm完成登录认证,里面插入操作Service、DAO代码
@RequestMapping(value = "/login", method = RequestMethod.POST)
public Result login(@RequestBody UserDO teacherDO) {
// 使用shiro框架进行认证
// 获取当前用户对象,状态为“未认证”
Subject subject = SecurityUtils.getSubject();
AuthenticationToken token = new UsernamePasswordToken(teacherDO.getTeacherUsername(), Md5Utils.toMD5(teacherDO.getTeacherPassword()));
try {
subject.login(token);
} catch (Exception e) {
e.printStackTrace();
return Result.build(ResultEnum.ERROR.getCode(), "用户名或密码错误!");
}
// 查询登录成功的数据,放到redis中
UserDO teacher = (UserDO) subject.getPrincipal();
Serializable sessionId = subject.getSession().getId();
Map<String, Object> dataMap = Maps.newHashMap();
dataMap.put("token", sessionId);
dataMap.put("teacher", teacher);
redisTemplate.opsForValue().set(teacher.getUsername(), sessionId);
return Result.ok("登陆成功!", dataMap);
}
而有多个用户表时 比如 学生 老师时 因为查询的是不同的数据库 所以可以创建一个枚举或静态类去标记要求查哪个表
public class UserType {
/** 老师 */
public static final String TEACHER = "teacher";
/** 学生 */
public static final String STUDENT = "student";
//..
}
@Getter
public enum UserType {
/**
* 学生
*/
STUDENT("student","学生"),
TEACHER("teacher","老师");
private String code;
private String msg;
UserType(String code, String msg) {
this.code = code;
this.msg = msg;
}
}
然后就要扩展UsernamePasswordToken 用来接收上面的参数
/**
* Description:自定义shiro-token重写类,用于多类型用户校验
*/
public class CustomLoginToken extends UsernamePasswordToken {
private static final long serialVersionUID = 2020457391511655213L;
private String loginType;
public CustomLoginToken() {}
public CustomLoginToken(final String username, final String password,
final String loginType) {
super(username, password);
this.loginType = loginType;
}
public String getLoginType() {
return loginType;
}
public void setLoginType(String loginType) {
this.loginType = loginType;
}
}
而后实现多个Realm
public class ExamRealm extends AuthorizingRealm {
@Autowired
private TeacherService teacherService;
@Autowired
private RoleService roleService;
@Autowired
private TeacherRoleService teacherRoleService;
@Autowired
private RoleAuthService roleAuthService;
/**
* 授权方法
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 获取登录中的用户
TeacherDO teacher = (TeacherDO) principalCollection.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 查询角色, 封装成集合
List<TeacherRoleDO> roleList = teacherRoleService.getByTeacher(teacher);
// Lambda表达式取出集合中指定元素封装成另一个集合
List<String> roleIds = roleList.stream().map(TeacherRoleDO::getTrRole).collect(Collectors.toList());
// 使用roleIds查询所有的角色,将角色名封装成集合
List<String> roleNames = roleService.listByIds(roleIds).stream().map(RoleDO::getRoleName).collect(Collectors.toList());
info.addRoles(roleNames);
// 根据roles查询权限
List<AuthDO> authList = roleAuthService.getByRoleIds(roleIds);
List<String> authCodes = authList.stream().map(AuthDO::getAuthCode).collect(Collectors.toList());
info.addStringPermissions(authCodes);
return info;
}
/**
* 认证
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 根据用户名查询数据库中的密码
UsernamePasswordToken passwordToken = (UsernamePasswordToken) token;
String username = passwordToken.getUsername();
QueryWrapper<TeacherDO> wrapper = new QueryWrapper<>();
wrapper.eq("teacher_username", username);
TeacherDO teacherDO = teacherService.getOne(wrapper);
if(teacherDO == null) {
// 用户名不存在
return null;
}
// 框架负责比对数据库中的密码和页面输入的密码是否一致
AuthenticationInfo info = new SimpleAuthenticationInfo(teacherDO, teacherDO.getTeacherPassword(), this.getName());
return info;
}
}
public class StudentRealm extends AuthorizingRealm {
@Autowired
private StudentService studentService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
/**
* 认证
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken passwordToken = (UsernamePasswordToken) token;
String username = passwordToken.getUsername();
QueryWrapper<StudentDO> wrapper = new QueryWrapper<>();
wrapper.eq("stu_number", username);
StudentDO studentDO = studentService.getOne(wrapper);
if(studentDO == null) {
// 用户名不存在
throw new UnknownAccountException();
}
// 框架负责比对数据库中的密码和页面输入的密码是否一致
AuthenticationInfo info = new SimpleAuthenticationInfo(studentDO, studentDO.getStuPassword(), this.getName());
return info;
}
}
然后在shiroConfig中进行添加配置
- 自定义的session管理器
/**
* 自定义shiro的session管理器
* @version 1.0
*/
public class MySessionManager extends DefaultWebSessionManager {
private static final String AUTHORIZATION = "Authorization";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
public MySessionManager() {
super();
}
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
//如果请求头中有 Authorization 则其值为sessionId
if (!StringUtils.isBlank(id)) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return id;
} else {
//否则按默认规则从cookie取sessionId
return super.getSessionId(request, response);
}
}
}
自定义的shiro过滤器 放行options方法
/** * 重写shiro过滤器,自动放行OPTIONS请求 * * @version 1.0 * @date: 2019/3/31 0031 下午 7:39 */ public class OptionsAuthenticationFilter extends PassThruAuthenticationFilter { @Override public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { HttpServletRequest req = (HttpServletRequest) request; if (req.getMethod().equals(RequestMethod.OPTIONS.name())) { return true; } return super.onPreHandle(request, response, mappedValue); } }
用户登录后,shiro首先去访问安全管理器
securityManager
一般web项目都用这个安全管理器做认证使用的,那么用户需要将自己实现的Realm写入,若只有一个Realm,则设置属性绑定realm,若有多个,则用realms而设置只是让Shiro知道你的这个项目有几个Realm,它管理认证校验时,一定会将多个Realm都参与认证 即 shiro会把你所添加的所有Realm全部去校验 所以要有自己的代码 只校验对应的Realm
org.apache.shiro.web.mgt.DefaultWebSecurityManager
按照shiro的源码,若安全管理器只配置一个Realm,则使用doSingleRealmAuthentication
方法进入Realm做单独认证;若有多个Realm时,则使用doMultiRealmAuthentication
方法,加载Collection
添加了多个Realm则 Shiro会进入这个方法org.apache.shiro.authc.pam.ModularRealmAuthenticator
,将多个Realm都读取到,并加载这些认证信息
所以步骤是
- 重写
ModularRealmAuthenticator
- 针对多Realm,找到指定待认证的Realm信息
手工调用
doSingleRealmAuthentication
让其只认证指定需要认证的那个,可通过loginType;import java.util.ArrayList; import java.util.Collection; import java.util.Map; import org.apache.shiro.ShiroException; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.pam.ModularRealmAuthenticator; import org.apache.shiro.realm.Realm; import org.apache.shiro.util.CollectionUtils; import com.fg.cloud.common.shiro.CustomLoginToken; /** * Description:全局shiro拦截分发realm */ public class CustomModularRealmAuthenticator extends ModularRealmAuthenticator { private Map<String, Object> definedRealms; @Override protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { // 判断getRealms()是否返回为空 assertRealmsConfigured(); // 强制转换回自定义的CustomizedToken CustomLoginToken token = (CustomLoginToken) authenticationToken; // 找到当前登录人的登录类型 String loginType = token.getLoginType(); // 所有Realm Collection<Realm> realms = getRealms(); // 找到登录类型对应的指定Realm Collection<Realm> typeRealms = new ArrayList<Realm>(); for (Realm realm : realms) { if (realm.getName().toLowerCase().contains(loginType)) typeRealms.add(realm); } // 判断是单Realm还是多Realm if (typeRealms.size() == 1) return doSingleRealmAuthentication(typeRealms.iterator().next(), token); else return doMultiRealmAuthentication(typeRealms, token); } /** * 判断realm是否为空 */ @Override protected void assertRealmsConfigured() throws IllegalStateException { this.definedRealms = this.getDefinedRealms(); if (CollectionUtils.isEmpty(this.definedRealms)) { throw new ShiroException("值传递错误!"); } } public Map<String, Object> getDefinedRealms() { return this.definedRealms; } public void setDefinedRealms(Map<String, Object> definedRealms) { this.definedRealms = definedRealms; } }
自定义修改了ModularRealmAuthenticator
后 则要去ShiroConfig中配置 否则shiro仍会按照默认的来执行
/**
* shiro配置类
*
* @author
*/
@Configuration
public class ShiroConfig {
/**
* 创建ShiroFilterFactoryBean
*/
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") SecurityManager securityManager) {
System.out.println("==================ShiroFilterFactoryBean====================");
// 设置安全管理器
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 配置自定义shiro过滤器
Map<String, Filter> optionsFilter = Maps.newHashMap();
OptionsAuthenticationFilter authenticationFilter = new OptionsAuthenticationFilter();
optionsFilter.put("authc", authenticationFilter);
shiroFilterFactoryBean.setFilters(optionsFilter);
/**
* 常用过滤器
* anon:无需认证可以访问
* authc:必须认证才能访问
* user:如果使用rememberMe的功能可以直接访问
* perms:该资源必须得到权限才可以访问
* role:该资源必须得到角色权限才可以访问
*/
Map<String, String> filterMap = Maps.newHashMap();
filterMap.put("/teacher/login", "anon");
filterMap.put("/student/login", "anon");
filterMap.put("/studentPaperDO", "anon");
filterMap.put("/logout", "logout");
filterMap.put("/upload/**", "anon");
filterMap.put("/upload", "anon");
filterMap.put("/file/**", "anon");
filterMap.put("/export/paper", "anon");
filterMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}
/**
* 创建DefaultSecurityManager 并进行ModularRealmAuthenticator的添加
*/
@Bean("securityManager")
public SecurityManager securityManager(@Qualifier("examReam") ExamRealm examRealm,@Qualifier("studentReam") StudentRealm studentRealm
,@Qualifier("modularRealmAuthenticator") ModularRealmAuthenticator modularRealmAuthenticator) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 自定义session管理 使用redis
securityManager.setSessionManager(sessionManager());
// 加入认证器
securityManager.setAuthenticator(modularRealmAuthenticator);
// 关联realm
securityManager.setRealm(examRealm);
securityManager.setRealm(studentRealm);
return securityManager;
}
/**
* 自定义modularRealmAuthenticator
*/
@Bean("modularRealmAuthenticator")
public ModularRealmAuthenticator modularRealmAuthenticator(){
CustomModularRealmAuthenticator authenticator = new CustomModularRealmAuthenticator();
HashMap<String,Object> hashMap = new HashMap<>();
// 添加自定义的Realm key,value形式
hashMap.put("teacher",examRealm());
hashMap.put("student",studentRealm());
authenticator.setDefinedRealms(hashMap);
// 配置策略,只要有一个Realam认证成功即可 并且返回认证信息
FirstSuccessfulStrategy strategy = new FirstSuccessfulStrategy();
authenticator.setAuthenticationStrategy(strategy);
return authenticator;
}
/**
* 自定义sessionManager
*/
@Bean("sessionManager")
public SessionManager sessionManager() {
MySessionManager mySessionManager = new MySessionManager();
mySessionManager.setGlobalSessionTimeout(CoreConstant.REDIS_TIMEOUT);
return mySessionManager;
}
/**
* 创建Realm 要加上@Bean
*/
@Bean("examReam")
public ExamRealm examRealm() {
return new ExamRealm();
}
@Bean("studentReam")
public StudentRealm studentRealm(){
return new StudentRealm();
}
@Bean
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public static DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
改写完后 就可以根据自己指定认证的去处理
异常
No SecurityManager accessible to the calling code
No SecurityManager accessible to the calling code, either bound to the org.apache.shiro.util.ThreadContext or as a vm static singleton. This is an invalid application configuration.
调用代码无法访问SecurityManager
解决方法
/**
*
* @return MethodInvokingFactoryBean 实例
*/
@Bean
public MethodInvokingFactoryBean methodInvokingFactoryBean() {
MethodInvokingFactoryBean bean = new MethodInvokingFactoryBean();
bean.setStaticMethod("org.apache.shiro.SecurityUtils.setSecurityManager");
bean.setArguments(securityManager());
return bean;
}
CastException
这个异常的主要原因是在自定义的Realm中设置的Principal
与通过
SecurityUtils.getSubject().getPrincipal()
获得的对象 不符合,需要对Realm中的认证
Authentication
方法的返回值中的对象进行更改
同时出现上述情况的原因可能是你配置了多Realm的情况,这时候需要进行多Realm的
授权
处理
public class CustomerAuthrizer extends ModularRealmAuthorizer {
@Override
public boolean isPermitted(PrincipalCollection principals, String permission) {
assertRealmsConfigured();
Object primaryPrincipal = principals.getPrimaryPrincipal();
for (Realm realm : getRealms()) {
if (!(realm instanceof Authorizer)) continue;
// 对应的类
if (primaryPrincipal instanceof Admin) {
// 自定义的Realm
if (realm instanceof AdminShiroRealm) {
return ((AdminShiroRealm) realm).isPermitted(principals, permission);
}
}
if (primaryPrincipal instanceof Member) {
if (realm instanceof CustomShiroRealm) {
return ((CustomShiroRealm) realm).isPermitted(principals, permission);
}
}
}
return false;
}
}
在Springboot中的ShiroConfig
中的securityManger
进行配置
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
CustomerAuthrizer customModularRealmAuthorizer = new CustomerAuthrizer();
customModularRealmAuthorizer.setRealms(shiroAuthorizerRealms);
securityManager.setAuthorizer(customModularRealmAuthorizer);
待参考文章