首页 > 编程语言 > Spring Boot 集成Shiro的多realm配置过程
2020
10-20

Spring Boot 集成Shiro的多realm配置过程

我在做毕设的时候采用shiro进行登录认证和权限管理的实现。其中需求涉及使用三个角色分别是:学生、教师、管理员。现在要三者实现分开登录。即需要三个Realm——StudentRealm和TeacherRealm、AdminRealm,分别处理学生、教师和管理员的验证功能。

但是正常情况下,当定义了多个Realm,无论是学生登录,教师登录,还是管理员登录,都会由这三个Realm共同处理。这是因为,当配置了多个Realm时,我们通常使用的认证器是shiro自带的org.apache.shiro.authc.pam.ModularRealmAuthenticator,其中决定使用的Realm的是doAuthenticate()方法,源代码如下:

protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
  assertRealmsConfigured();
  Collection<Realm> realms = getRealms();
  if (realms.size() == 1) {
   return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
  } else {
   return doMultiRealmAuthentication(realms, authenticationToken);
  }
 }

上述代码的意思就是如果有多个Realm就会使用所有配置的Realm。 只有一个的时候,就直接使用当前的Realm。

为了实现需求,我会创建一个org.apache.shiro.authc.pam.ModularRealmAuthenticator的子类,并重写doAuthenticate()方法,让特定的Realm完成特定的功能。如何区分呢?我会同时创建一个org.apache.shiro.authc.UsernamePasswordToken的子类,在其中添加一个字段loginType,用来标识登录的类型,即是学生登录、教师登录,还是管理员登录。具体步骤如下(我的代码使用的是Groovy):

enum LoginType {
 STUDENT("Student"), ADMIN("Admin"), TEACHER("Teacher")

 private String type

 private LoginType(String type) {
  this.type = type
 }

 @Override
 public String toString() {
  return this.type.toString()
 }
}

接下来新建org.apache.shiro.authc.UsernamePasswordToken的子类UserToken

import org.apache.shiro.authc.UsernamePasswordToken

class UserToken extends UsernamePasswordToken {

 //登录类型,判断是学生登录,教师登录还是管理员登录
 private String loginType

 public UserToken(final String username, final String password,String loginType) {
  super(username,password)
  this.loginType = loginType
 }

 public String getLoginType() {
  return loginType
 }
 public void setLoginType(String loginType) {
  this.loginType = loginType
 }
}

第三步:新建org.apache.shiro.authc.pam.ModularRealmAuthenticator的子类UserModularRealmAuthenticator:

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.slf4j.Logger
import org.slf4j.LoggerFactory

/**
 * 当配置了多个Realm时,我们通常使用的认证器是shiro自带的org.apache.shiro.authc.pam.ModularRealmAuthenticator,其中决定使用的Realm的是doAuthenticate()方法
 *
 * 自定义Authenticator
 * 注意,当需要分别定义处理学生和教师和管理员验证的Realm时,对应Realm的全类名应该包含字符串“Student”“Teacher”,或者“Admin”。
 * 并且,他们不能相互包含,例如,处理学生验证的Realm的全类名中不应该包含字符串"Admin"。
 */
class UserModularRealmAuthenticator extends ModularRealmAuthenticator {

 private static final Logger logger = LoggerFactory.getLogger(UserModularRealmAuthenticator.class)

 @Override
 protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken)
   throws AuthenticationException {
  logger.info("UserModularRealmAuthenticator:method doAuthenticate() execute ")
  // 判断getRealms()是否返回为空
  assertRealmsConfigured()
  // 强制转换回自定义的CustomizedToken
  UserToken userToken = (UserToken) authenticationToken
  // 登录类型
  String loginType = userToken?.getLoginType()
  // 所有Realm
  Collection<Realm> realms = getRealms()
  // 登录类型对应的所有Realm
  Collection<Realm> typeRealms = new ArrayList<>()
  for (Realm realm : realms) {
   if (realm?.getName()?.contains(loginType))
    typeRealms?.add(realm)
  }

  // 判断是单Realm还是多Realm
  if (typeRealms?.size() == 1){
   logger.info("doSingleRealmAuthentication() execute ")
   return doSingleRealmAuthentication(typeRealms?.get(0), userToken)
  }
  else{
   logger.info("doMultiRealmAuthentication() execute ")
   return doMultiRealmAuthentication(typeRealms, userToken)
  }
 }
}

第四步:创建分别处理学生登录和教师登录、管理员登录的Realm:
我这里直接贴出了我项目中的代码,你们可以根据具体的需求进行操作。
AdminShiroRealm :

package com.ciyou.edu.config.shiro.admin
import com.ciyou.edu.config.shiro.common.UserToken
import com.ciyou.edu.entity.Admin
import com.ciyou.edu.service.AdminService
import com.ciyou.edu.service.PermissionService
import org.apache.shiro.authc.AuthenticationException
import org.apache.shiro.authc.AuthenticationInfo
import org.apache.shiro.authc.AuthenticationToken
import org.apache.shiro.authc.SimpleAuthenticationInfo
import org.apache.shiro.authc.UnknownAccountException
import org.apache.shiro.authz.AuthorizationException
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.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Lazy


class AdminShiroRealm extends AuthorizingRealm {

  private static final Logger logger = LoggerFactory.getLogger(AdminShiroRealm.class)
  @Autowired
  @Lazy
  private AdminService adminService


 @Autowired
 @Lazy
 private PermissionService permissionService



 @Override
 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

  logger.info("开始Admin身份认证...")
  UserToken userToken = (UserToken)token
  String adminName = userToken?.getUsername() //获取用户名,默认和login.html中的adminName对应。
  Admin admin = adminService?.findByAdminName(adminName)

  if (admin == null) {
   //没有返回登录用户名对应的SimpleAuthenticationInfo对象时,就会在LoginController中抛出UnknownAccountException异常
   throw new UnknownAccountException("用户不存在!")
  }

  //验证通过返回一个封装了用户信息的AuthenticationInfo实例即可。
  SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
    admin, //用户信息
    admin?.getPassword(), //密码
    getName() //realm name
  )
  authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(admin?.getAdminName())) //设置盐
  logger.info("返回Admin认证信息:" + authenticationInfo)
  return authenticationInfo
 }

//当访问到页面的时候,链接配置了相应的权限或者shiro标签才会执行此方法否则不会执行
 @Override
 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

  logger.info("开始Admin权限授权(进行权限验证!!)")
  if (principals == null) {
   throw new AuthorizationException("PrincipalCollection method argument cannot be null.")
  }
  SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo()
  if(principals?.getPrimaryPrincipal() instanceof Admin){
   Admin admin = (Admin) principals?.getPrimaryPrincipal()
   logger.info("当前Admin :" + admin )
   authorizationInfo?.addRole("Admin")
   //每次都从数据库重新查找,确保能及时更新权限
   admin?.setPermissionList(permissionService?.findPermissionByAdmin(admin?.getAdminId()))
   admin?.getPermissionList()?.each {current_Permission ->
    authorizationInfo?.addStringPermission(current_Permission?.getPermission())
   }
   logger.info("当前Admin授权角色:" +authorizationInfo?.getRoles() + ",权限:" + authorizationInfo?.getStringPermissions())
   return authorizationInfo
  }
 }
}

TeacherShiroRealm :

package com.ciyou.edu.config.shiro.teacher

import com.ciyou.edu.config.shiro.common.UserToken
import com.ciyou.edu.entity.Teacher
import com.ciyou.edu.service.TeacherService
import org.apache.shiro.authc.*
import org.apache.shiro.authz.AuthorizationException
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.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Lazy


class TeacherShiroRealm extends AuthorizingRealm {
 private static final Logger logger = LoggerFactory.getLogger(TeacherShiroRealm.class)

 //在自定义Realm中注入的Service声明中加入@Lazy注解即可解决@cacheble注解无效问题
 //解决同时使用Redis缓存数据和缓存shiro时,@cacheble无效的问题
  @Autowired
  @Lazy
  private TeacherService teacherService



 @Override
 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

  logger.info("开始Teacher身份认证..")
  UserToken userToken = (UserToken)token
  String teacherId = userToken?.getUsername()
  Teacher teacher = teacherService?.findByTeacherId(teacherId)

  if (teacher == null) {
   //没有返回登录用户名对应的SimpleAuthenticationInfo对象时,就会在LoginController中抛出UnknownAccountException异常
   throw new UnknownAccountException("用户不存在!")
  }

  //验证通过返回一个封装了用户信息的AuthenticationInfo实例即可。
  SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
    teacher, //用户信息
    teacher?.getPassword(), //密码
    getName() //realm name
  )
  authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(teacher?.getTeacherId())) //设置盐

  return authenticationInfo
 }

//当访问到页面的时候,链接配置了相应的权限或者shiro标签才会执行此方法否则不会执行
 @Override
 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
  logger.info("开始Teacher权限授权")
  if (principals == null) {
   throw new AuthorizationException("PrincipalCollection method argument cannot be null.")
  }
  SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo()
  if(principals?.getPrimaryPrincipal() instanceof Teacher){
   authorizationInfo?.addRole("Teacher")
   return authorizationInfo
  }
 }
}

StudentShiroRealm :

package com.ciyou.edu.config.shiro.student

import com.ciyou.edu.config.shiro.common.UserToken
import com.ciyou.edu.entity.Student
import com.ciyou.edu.service.StudentService
import org.apache.shiro.authc.*
import org.apache.shiro.authz.AuthorizationException
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.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Lazy


class StudentShiroRealm extends AuthorizingRealm {
 private static final Logger logger = LoggerFactory.getLogger(StudentShiroRealm.class)

 //在自定义Realm中注入的Service声明中加入@Lazy注解即可解决@cacheble注解无效问题
 //解决同时使用Redis缓存数据和缓存shiro时,@cacheble无效的问题
  @Autowired
  @Lazy
  private StudentService studentService



 @Override
 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

  logger.info("开始Student身份认证..")
  UserToken userToken = (UserToken)token
  String studentId = userToken?.getUsername()
  Student student = studentService?.findByStudentId(studentId)

  if (student == null) {
   //没有返回登录用户名对应的SimpleAuthenticationInfo对象时,就会在LoginController中抛出UnknownAccountException异常
   throw new UnknownAccountException("用户不存在!")
  }

  //验证通过返回一个封装了用户信息的AuthenticationInfo实例即可。
  SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
    student, //用户信息
    student?.getPassword(), //密码
    getName() //realm name
  )
  authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(student?.getStudentId())) //设置盐

  return authenticationInfo
 }

//当访问到页面的时候,链接配置了相应的权限或者shiro标签才会执行此方法否则不会执行
 @Override
 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
  logger.info("开始Student权限授权")
  if (principals == null) {
   throw new AuthorizationException("PrincipalCollection method argument cannot be null.")
  }
  SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo()
  if(principals?.getPrimaryPrincipal() instanceof Student){
   authorizationInfo?.addRole("Student")
   return authorizationInfo
  }
 }
}

接下来是对Shiro进行多realm的注解配置。
这里直接贴出我项目中的代码。

 

上面是我进行shiro进行配置的类,下面是主要的一些代码:

//SecurityManager 是 Shiro 架构的核心,通过它来链接Realm和用户(文档中称之为Subject.)
 @Bean
 public SecurityManager securityManager() {
  DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager()
  //设置realm.
  securityManager.setAuthenticator(modularRealmAuthenticator())
  List<Realm> realms = new ArrayList<>()
  //添加多个Realm
  realms.add(adminShiroRealm())
  realms.add(teacherShiroRealm())
  realms.add(studentShiroRealm())
  securityManager.setRealms(realms)
  // 自定义缓存实现 使用redis
  securityManager.setCacheManager(cacheManager())
  // 自定义session管理 使用redis
  securityManager.setSessionManager(sessionManager())
  //注入记住我管理器;
  securityManager.setRememberMeManager(rememberMeManager())
  return securityManager
 }

 /**
  * 系统自带的Realm管理,主要针对多realm
  * */
 @Bean
 public ModularRealmAuthenticator modularRealmAuthenticator(){
  //自己重写的ModularRealmAuthenticator
  UserModularRealmAuthenticator modularRealmAuthenticator = new UserModularRealmAuthenticator()
  modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy())
  return modularRealmAuthenticator
 }

 @Bean
 public AdminShiroRealm adminShiroRealm() {
  AdminShiroRealm adminShiroRealm = new AdminShiroRealm()
  adminShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher())//设置解密规则
  return adminShiroRealm
 }

 @Bean
 public StudentShiroRealm studentShiroRealm() {
  StudentShiroRealm studentShiroRealm = new StudentShiroRealm()
  studentShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher())//设置解密规则
  return studentShiroRealm
 }

 @Bean
 public TeacherShiroRealm teacherShiroRealm() {
  TeacherShiroRealm teacherShiroRealm = new TeacherShiroRealm()
  teacherShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher())//设置解密规则
  return teacherShiroRealm
 }

 //因为我们的密码是加过密的,所以,如果要Shiro验证用户身份的话,需要告诉它我们用的是md5加密的,并且是加密了两次。同时我们在自己的Realm中也通过SimpleAuthenticationInfo返回了加密时使用的盐。这样Shiro就能顺利的解密密码并验证用户名和密码是否正确了。
 @Bean
 public HashedCredentialsMatcher hashedCredentialsMatcher() {
  HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher()
  hashedCredentialsMatcher.setHashAlgorithmName("md5")//散列算法:这里使用MD5算法;
  hashedCredentialsMatcher.setHashIterations(2)//散列的次数,比如散列两次,相当于 md5(md5(""));
  return hashedCredentialsMatcher;
 }

接下来就是Controller中实现登录的功能了,这里我只贴出我项目中Admin登录的代码:

package com.ciyou.edu.controller.admin

import com.ciyou.edu.config.shiro.common.LoginType
import com.ciyou.edu.config.shiro.common.UserToken
import com.ciyou.edu.entity.Admin
import com.ciyou.edu.utils.JSONUtil
import org.apache.shiro.SecurityUtils
import org.apache.shiro.authc.AuthenticationException
import org.apache.shiro.subject.Subject
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestMethod
import org.springframework.web.bind.annotation.ResponseBody


/**
 * @Author C.
 * @Date 2018-02-02 20:46
 * admin登录Controller
 */
@Controller
class AdminLoginController {

 private static final Logger logger = LoggerFactory.getLogger(AdminLoginController.class)
 private static final String ADMIN_LOGIN_TYPE = LoginType.ADMIN.toString()

 /**
  * admin登录
  * @param admin
  * @return 登录结果
  */
 @RequestMapping(value="/adminLogin",method=RequestMethod.POST, produces="application/json;charset=UTF-8")
 @ResponseBody
 public String loginAdmin(Admin admin){
  logger.info("登录Admin: " + admin)
  //后台校验提交的用户名和密码
  if(!admin?.getAdminName() || admin?.adminName?.trim() == ""){
   return JSONUtil.returnFailReuslt("账号不能为空")
  }else if(!admin?.getPassword() || admin?.getPassword()?.trim() == ""){
   return JSONUtil.returnFailReuslt("密码不能为空")
  }else if(admin?.getAdminName()?.length() < 3 || admin?.getAdminName()?.length() >15){
   return JSONUtil.returnFailReuslt("账号长度必须在3~15之间")
  }else if(admin?.getPassword()?.length() < 3 || admin?.getPassword()?.length() >15){
   return JSONUtil.returnFailReuslt("密码长度必须在3~15之间")
  }

  //获取Subject实例对象
  //在shiro里面所有的用户的会话信息都会由Shiro来进行控制,那么也就是说只要是与用户有关的一切的处理信息操作都可以通过Shiro取得,
  // 实际上可以取得的信息可以有用户名、主机名称等等,这所有的信息都可以通过Subject接口取得
  Subject subject = SecurityUtils.getSubject()

  //将用户名和密码封装到继承了UsernamePasswordToken的userToken
  UserToken userToken = new UserToken(admin?.getAdminName(), admin?.getPassword(), ADMIN_LOGIN_TYPE)
  userToken.setRememberMe(false)
  try {
   //认证
   // 传到ModularRealmAuthenticator类中,然后根据ADMIN_LOGIN_TYPE传到AdminShiroRealm的方法进行认证
   subject?.login(userToken)
   //Admin存入session
   SecurityUtils.getSubject()?.getSession()?.setAttribute("admin",(Admin)subject?.getPrincipal())
   return JSONUtil.returnSuccessResult("登录成功")
  } catch (AuthenticationException e) {
   //认证失败就会抛出AuthenticationException这个异常,就对异常进行相应的操作,这里的处理是抛出一个自定义异常ResultException
   //到时候我们抛出自定义异常ResultException,用户名或者密码错误
   logger.info("认证错误:" + e.getMessage())
   return JSONUtil.returnFailReuslt("账号或者密码错误")
  }
 }

 @RequestMapping(value="/admin/adminLogout")
 public String logoutAdmin(){
  SecurityUtils.getSubject()?.logout()
  return "redirect:/adminLogin"
 }
}

现在Spring Boot中集成Shiro实现多realm配置就完成了。

感谢以下博文,在我学习的过程中给了我很多帮助,我的博文也有一些内容参考他们的,还不够清楚的读者可以参考:
shiro实现不同身份使用不同Realm进行验证
SpringBoot+Shiro学习之数据库动态权限管理和Redis缓存
Springboot多realm集成,无ini文件,无xml配置

想看项目具体源码,或者对我项目感兴趣的可以查看:CIYOU

到此这篇关于Spring Boot 集成Shiro的多realm配置过程的文章就介绍到这了,更多相关Spring Boot多realm配置内容请搜索自学编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持自学编程网!

编程技巧