目录

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中进行添加配置

  1. 自定义的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);
        }
    }

}

  1. 自定义的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);
    }
    }
    
  2. 用户登录后,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认证

添加了多个Realm则 Shiro会进入这个方法org.apache.shiro.authc.pam.ModularRealmAuthenticator,将多个Realm都读取到,并加载这些认证信息

所以步骤是

  1. 重写ModularRealmAuthenticator
  2. 针对多Realm,找到指定待认证的Realm信息
  3. 手工调用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);

待参考文章

  1. https://juejin.im/post/5ac78b31f265da237411387e

  2. https://www.cnblogs.com/sunshine-2015/p/5515429.html