上一篇文章介绍了
SpringBoot整合Shiro,通过用户、角色、权限三者关联实现权限管理
本篇文章主要介绍 Shiro 多 realm,根据不同的登录类型指定不同的 realm。
所谓免密登录,就是区别正常的密码登录。比如,我现在要实现第三方登录,当验证了是张三,现在要让他通过 shiro 的 subject.login(),但是不知道他的密码(密码加密了),我们不能拿数据库里的密码去登录,除非重新写 Realm。
所以需要多个 Realm,一个是密码登录(shiro会根据用户的输入的密码和加密方法加密后比较);一个免密登录(允许使用数据库密码登录,shiro不进行任何加密)。
实现的过程简单说下:
重写 UsernamePasswordToken,加一个 loginType 属性,subject.login() 的时候传入 loginType; 重写 ModularRealmAuthenticator 中的 doAuthenticate() 方法,根据传进来的 loginType 来指定使用哪个 Realm。
一、Shiro 配置
1.两个 Realm
NormalRealm.java 密码登录的 Realm
- package com.liuyanzhao.sens.config;
- import cn.hutool.core.date.DateUnit;
- import cn.hutool.core.date.DateUtil;
- import cn.hutool.core.lang.Validator;
- import cn.hutool.core.util.ObjectUtil;
- import cn.hutool.extra.servlet.ServletUtil;
- import cn.hutool.http.HtmlUtil;
- import com.liuyanzhao.sens.entity.Permission;
- import com.liuyanzhao.sens.entity.Role;
- import com.liuyanzhao.sens.entity.User;
- import com.liuyanzhao.sens.model.dto.JsonResult;
- import com.liuyanzhao.sens.model.dto.LogsRecord;
- import com.liuyanzhao.sens.model.enums.CommonParamsEnum;
- import com.liuyanzhao.sens.model.enums.ResultCodeEnum;
- import com.liuyanzhao.sens.model.enums.TrueFalseEnum;
- import com.liuyanzhao.sens.service.PermissionService;
- import com.liuyanzhao.sens.service.RoleService;
- import com.liuyanzhao.sens.service.UserService;
- import com.liuyanzhao.sens.utils.LocaleMessageUtil;
- import com.liuyanzhao.sens.utils.Md5Util;
- import lombok.extern.slf4j.Slf4j;
- import org.apache.commons.lang3.StringUtils;
- import org.apache.shiro.authc.*;
- 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.Date;
- import java.util.List;
- /**
- * 默认的realm
- *
- * @author 言曌
- * @date 2018/9/1 上午10:47
- */
- @Slf4j
- public class NormalRealm extends AuthorizingRealm {
- @Autowired
- private UserService userService;
- @Autowired
- private RoleService roleService;
- @Autowired
- private PermissionService permissionService;
- @Autowired
- private LocaleMessageUtil localeMessageUtil;
- /**
- * 认证信息(身份验证) Authentication 是用来验证用户身份
- */
- @Override
- protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
- log.info("认证-->MyShiroRealm.doGetAuthenticationInfo()");
- //1.验证用户名
- User user = null;
- String loginName = (String) token.getPrincipal();
- if (Validator.isEmail(loginName)) {
- user = userService.findByEmail(loginName);
- } else {
- user = userService.findByUserName(loginName);
- }
- if (user == null) {
- //用户不存在
- log.info("用户不存在! 登录名:{}, 密码:{}", loginName, token.getCredentials());
- return null;
- }
- //2.首先判断是否已经被禁用已经是否已经过了10分钟
- Date loginLast = DateUtil.date();
- if (null != user.getLoginLast()) {
- loginLast = user.getLoginLast();
- }
- Long between = DateUtil.between(loginLast, DateUtil.date(), DateUnit.MINUTE);
- if (StringUtils.equals(user.getLoginEnable(), TrueFalseEnum.FALSE.getDesc()) && (between < CommonParamsEnum.TEN.getValue())) {
- log.info("账号已锁定! 登录名:{}, 密码:{}", loginName, token.getCredentials());
- throw new LockedAccountException(localeMessageUtil.getMessage("code.admin.login.disabled"));
- }
- userService.updateUserLoginLast(user, DateUtil.date());
- //3.封装authenticationInfo,准备验证密码
- SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
- user, // 用户名
- user.getUserPass(), // 密码
- ByteSource.Util.bytes("sens"), // 盐
- getName() // realm name
- );
- System.out.println("realName:" + getName());
- return authenticationInfo;
- }
- @Override
- protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
- log.info("授权-->MyShiroRealm.doGetAuthorizationInfo()");
- SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
- User user = (User) principals.getPrimaryPrincipal();
- List<Role> roles = roleService.listRolesByUserId(user.getUserId());
- for (Role role : roles) {
- authorizationInfo.addRole(role.getRole());
- List<Permission> permissions = permissionService.listPermissionsByRoleId(role.getId());
- for (Permission p : permissions) {
- authorizationInfo.addStringPermission(p.getPermission());
- }
- }
- return authorizationInfo;
- }
- }
FreeRealm.java 密码不加密的 Realm
- package com.liuyanzhao.sens.config;
- import cn.hutool.core.date.DateUnit;
- import cn.hutool.core.date.DateUtil;
- import cn.hutool.core.lang.Validator;
- import com.liuyanzhao.sens.entity.Permission;
- import com.liuyanzhao.sens.entity.Role;
- import com.liuyanzhao.sens.entity.User;
- import com.liuyanzhao.sens.model.enums.CommonParamsEnum;
- import com.liuyanzhao.sens.model.enums.TrueFalseEnum;
- import com.liuyanzhao.sens.service.PermissionService;
- import com.liuyanzhao.sens.service.RoleService;
- import com.liuyanzhao.sens.service.UserService;
- import com.liuyanzhao.sens.utils.LocaleMessageUtil;
- import lombok.extern.slf4j.Slf4j;
- import org.apache.commons.lang3.StringUtils;
- import org.apache.shiro.authc.*;
- 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.Date;
- import java.util.List;
- /**
- * 免密登录,输入的密码和原密码一致
- *
- * @author 言曌
- * @date 2018/9/1 上午10:47
- */
- @Slf4j
- public class FreeRealm extends AuthorizingRealm {
- @Autowired
- private UserService userService;
- @Autowired
- private RoleService roleService;
- @Autowired
- private PermissionService permissionService;
- @Autowired
- private LocaleMessageUtil localeMessageUtil;
- /**
- * 认证信息(身份验证) Authentication 是用来验证用户身份
- */
- @Override
- protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
- //1.验证用户名
- User user = null;
- String loginName = (String) token.getPrincipal();
- if (Validator.isEmail(loginName)) {
- user = userService.findByEmail(loginName);
- } else {
- user = userService.findByUserName(loginName);
- }
- if (user == null) {
- //用户不存在
- log.info("第三方登录,用户不存在! 登录名:{}, 密码:{}", loginName,token.getCredentials());
- return null;
- }
- //3.封装authenticationInfo,准备验证密码
- SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
- user, // 用户名
- user.getUserPass(), // 密码
- null,
- getName() // realm name
- );
- return authenticationInfo;
- }
- @Override
- protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
- System.out.println("权限配置-->MyShiroRealm.doGetAuthorizationInfo()");
- SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
- User user = (User) principals.getPrimaryPrincipal();
- List<Role> roles = roleService.listRolesByUserId(user.getUserId());
- for (Role role : roles) {
- authorizationInfo.addRole(role.getRole());
- List<Permission> permissions = permissionService.listPermissionsByRoleId(role.getId());
- for (Permission p : permissions) {
- authorizationInfo.addStringPermission(p.getPermission());
- }
- }
- return authorizationInfo;
- }
- }
2.ShiroConfig
主要关注65-86行
- package com.liuyanzhao.sens.config;
- import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
- 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 java.util.ArrayList;
- import java.util.LinkedHashMap;
- import java.util.List;
- import java.util.Map;
- /**
- * @author 言曌
- * @date 2018/8/20 上午6:19
- */
- @Configuration
- public class ShiroConfig {
- @Bean
- public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
- System.out.println("ShiroConfiguration.shirFilter()");
- ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
- shiroFilterFactoryBean.setSecurityManager(securityManager);
- //拦截器.
- Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();
- // 配置不会被拦截的链接 顺序判断
- filterChainDefinitionMap.put("/static/**", "anon");
- filterChainDefinitionMap.put("/upload/**", "anon");
- filterChainDefinitionMap.put("/favicon.ico", "anon");
- filterChainDefinitionMap.put("/favicon.ico", "anon");
- filterChainDefinitionMap.put("/admin/login", "anon");
- filterChainDefinitionMap.put("/admin/getLogin", "anon");
- //配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了
- filterChainDefinitionMap.put("/logout", "logout");
- //<!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;
- //<!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
- filterChainDefinitionMap.put("/admin/**", "authc");
- filterChainDefinitionMap.put("/backup/**", "authc");
- filterChainDefinitionMap.put("/**", "anon");
- shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
- // 如果不设置默认会自动寻找Web工程根目录下的"/login"页面
- shiroFilterFactoryBean.setLoginUrl("/admin/login");
- // 登录成功后要跳转的链接
- shiroFilterFactoryBean.setSuccessUrl("/");
- //未授权界面;
- shiroFilterFactoryBean.setUnauthorizedUrl("/403");
- return shiroFilterFactoryBean;
- }
- @Bean
- public SecurityManager securityManager() {
- DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
- securityManager.setAuthenticator(modularRealmAuthenticator());
- List<Realm> realms = new ArrayList<>();
- //密码登录realm
- realms.add(normalRealm());
- //免密登录realm
- realms.add(freeRealm());
- securityManager.setRealms(realms);
- return securityManager;
- }
- /**
- * 系统自带的Realm管理,主要针对多realm
- * */
- @Bean
- public ModularRealmAuthenticator modularRealmAuthenticator(){
- //自己重写的ModularRealmAuthenticator
- UserModularRealmAuthenticator modularRealmAuthenticator = new UserModularRealmAuthenticator();
- modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
- return modularRealmAuthenticator;
- }
- /**
- * 需要密码登录的realm
- *
- * @return MyShiroRealm
- */
- @Bean
- public NormalRealm normalRealm() {
- NormalRealm normalRealm = new NormalRealm();
- normalRealm.setCredentialsMatcher(hashedCredentialsMatcher());
- return normalRealm;
- }
- /**
- * 免密登录realm
- *
- * @return MyShiroRealm
- */
- @Bean
- public FreeRealm freeRealm() {
- FreeRealm realm = new FreeRealm();
- //不需要加密,直接用数据库密码进行登录
- return realm;
- }
- /**
- * 凭证匹配器
- * (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
- * 所以我们需要修改下doGetAuthenticationInfo中的代码;
- * )
- * @return
- */
- @Bean
- public HashedCredentialsMatcher hashedCredentialsMatcher(){
- HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
- hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法;
- hashedCredentialsMatcher.setHashIterations(10);//散列的次数,md5("")
- return hashedCredentialsMatcher;
- }
- /**
- * 开启shiro aop注解支持.
- * 使用代理方式;所以需要开启代码支持;
- * @param securityManager
- * @return
- */
- /** * Shiro生命周期处理器 * @return */
- @Bean
- public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
- return new LifecycleBeanPostProcessor();
- }
- @Bean
- @DependsOn({"lifecycleBeanPostProcessor"})
- public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
- DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
- advisorAutoProxyCreator.setProxyTargetClass(true);
- return advisorAutoProxyCreator;
- }
- @Bean
- public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(){
- AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
- authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
- return authorizationAttributeSourceAdvisor;
- }
- }
多 Realm 模式需要重写 ModularRealmAuthenticator
3.重写ModularRealmAuthenticator
UserModularRealmAuthenticator.java
- package com.liuyanzhao.sens.config;
- import com.liuyanzhao.sens.model.dto.UserToken;
- 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 java.util.ArrayList;
- import java.util.Collection;
- import java.util.List;
- /**
- * @author 言曌
- * @date 2019/1/24 下午4:19
- */
- public class UserModularRealmAuthenticator extends ModularRealmAuthenticator {
- @Override
- protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken)
- throws AuthenticationException {
- System.out.println("UserModularRealmAuthenticator:method doAuthenticate() execute ");
- // 判断getRealms()是否返回为空
- assertRealmsConfigured();
- // 强制转换回自定义的CustomizedToken
- UserToken userToken = (UserToken) authenticationToken;
- // 登录类型
- String loginType = userToken.getLoginType();
- // 所有Realm
- Collection<Realm> realms = getRealms();
- // 登录类型对应的所有Realm
- List<Realm> typeRealms = new ArrayList<>();
- for (Realm realm : realms) {
- if (realm.getName().contains(loginType)) {
- typeRealms.add(realm);
- }
- }
- // 判断是单Realm还是多Realm
- if (typeRealms.size() == 1){
- System.out.println("doSingleRealmAuthentication() execute ");
- return doSingleRealmAuthentication(typeRealms.get(0), userToken);
- }
- else{
- System.out.println("doMultiRealmAuthentication() execute ");
- return doMultiRealmAuthentication(typeRealms, userToken);
- }
- }
- }
4.枚举类 LoginType
LoginType.java
- package com.liuyanzhao.sens.model.enums;
- /**
- * @author 言曌
- * @date 2019/1/24 下午5:08
- */
- public enum LoginType {
- /**
- * 密码登录
- */
- NORMAL("Normal"),
- /**
- * 免密码登录
- */
- FREE("Free");
- private String desc;
- LoginType(String desc) {
- this.desc = desc;
- }
- public String getDesc() {
- return desc;
- }
- }
5.自定义UsernamePasswordToken
UserToken.java
- package com.liuyanzhao.sens.model.dto;
- import lombok.Data;
- import org.apache.shiro.authc.UsernamePasswordToken;
- /**
- *
- * 自定义UsernamePasswordToken
- * 必须传loginType
- *
- * @author 言曌
- * @date 2019/1/24 下午4:20
- */
- @Data
- public class UserToken extends UsernamePasswordToken {
- private String loginType;
- public UserToken() {
- }
- public UserToken(final String username, final String password,
- final String loginType) {
- super(username, password);
- this.loginType = loginType;
- }
- }
二、登录
1.正常的密码登录
- @PostMapping(value = "/getLogin")
- @ResponseBody
- public JsonResult getLogin(@ModelAttribute("loginName") String loginName, @ModelAttribute("loginPwd") String loginPwd) {
- Subject subject = SecurityUtils.getSubject();
- UserToken token = new UserToken(loginName, loginPwd, LoginType.FREE.getDesc());
- try {
- subject.login(token);
- if (subject.isAuthenticated()) {
- //登录成功,修改登录错误次数为0
- User user = (User) subject.getPrincipal();
- userService.updateUserLoginNormal(user);
- return new JsonResult(ResultCodeEnum.SUCCESS.getCode(),"登录成功");
- }
- } catch (UnknownAccountException e) {
- ...
- }
- ...
- }
2.第三方登录,授权成功后,免密登录
- package com.liuyanzhao.sens.web.controller.front;
- import cn.hutool.core.date.DateUtil;
- import cn.hutool.extra.servlet.ServletUtil;
- import com.liuyanzhao.sens.entity.Log;
- import com.liuyanzhao.sens.entity.ThirdAppBind;
- import com.liuyanzhao.sens.entity.User;
- import com.liuyanzhao.sens.model.dto.LogsRecord;
- import com.liuyanzhao.sens.model.dto.UserToken;
- import com.liuyanzhao.sens.model.enums.BindTypeEnum;
- import com.liuyanzhao.sens.model.enums.LoginType;
- import com.liuyanzhao.sens.service.*;
- import com.liuyanzhao.sens.utils.Response;
- import lombok.extern.slf4j.Slf4j;
- import org.apache.shiro.SecurityUtils;
- import org.apache.shiro.subject.Subject;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Controller;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.RequestParam;
- import javax.servlet.http.HttpServletRequest;
- /**
- * @author 言曌
- * @date 2018/5/9 下午2:59
- */
- @Controller
- @Slf4j
- public class AuthController {
- @Autowired
- private QQAuthService qqAuthService;
- @Autowired
- private UserService userService;
- @Autowired
- private ThirdAppBindService thirdAppBindService;
- @Autowired
- private LogService logService;
- /**
- * 第三方授权后会回调此方法,并将code传过来
- *
- * @param code code
- * @return
- */
- @GetMapping("/oauth/qq/callback")
- public String oauthByQQ(@RequestParam(value = "code") String code, HttpServletRequest request) {
- Response<String> tokenResponse = qqAuthService.getAccessToken(code);
- if (tokenResponse.isSuccess()) {
- Response<String> openidResponse = qqAuthService.getOpenId(tokenResponse.getData());
- if (openidResponse.isSuccess()) {
- //根据openId去找关联的用户
- ThirdAppBind bind = thirdAppBindService.findByAppTypeAndOpenId(BindTypeEnum.QQ.getValue(), openidResponse.getData());
- if (bind != null && bind.getUserId() != null) {
- //执行Login操作
- User user = userService.findByUserId(bind.getUserId());
- if (user != null) {
- Subject subject = SecurityUtils.getSubject();
- UserToken userToken = new UserToken(user.getUserName(), user.getUserPass(), LoginType.FREE.getDesc());
- try {
- subject.login(userToken);
- } catch (Exception e) {
- e.printStackTrace();
- log.error("第三方登录(QQ)免密码登录失败, cause:{}", e);
- return "redirect:/admin/login";
- }
- logService.saveByLog(new Log(LogsRecord.LOGIN, LogsRecord.LOGIN_SUCCESS + "(QQ登录)", ServletUtil.getClientIP(request), DateUtil.date()));
- log.info("用户[{}]登录成功(QQ登录)。", user.getUserDisplayName());
- return "redirect:/admin";
- }
- }
- }
- }
- return "redirect:/admin/login";
- }
- }
主要关注61-71行
2019年12月09日 12:00:37
问题我自己解决了 你的UserModularRealmAuthenticatorif (realm.getName().contains(loginType)) { 这句代码会出现bug,此时获取的realm.getName有大小写 2个解决方案 1. if (realm.getName().toLowerCase().contains(loginType)) { 转为小写,不然即是永远找不到对应的realm! 2.强制命名realm的name 在shiro的config配置类中 @Bean public DefaultUserRealm getDefaultUserRealm() { DefaultUserRealm defaultUserRealm = new DefaultUserRealm(); defaultUserRealm.setName(LoginType.DEFAULT.getDesc()); //此处设定shiro的Name! return defaultUserRealm; } 希望通过审查,给更多人看到
2019年12月06日 15:19:18
这对我有用,这个问题几乎困扰了我1天 但是有一个问题 你的案例中重写的ModularRealmAuthenticator中, 对于realm的判断是根据传入的 new UserToken(loginCode, password, LoginType.FREE.getDesc()); 中的LoginType.FREE.getDesc()来决定的吗? 我的案例中虽然没有采用你的realm名称, 但在类似的登录情景中,在修改了LoginType后实行subject.login()登陆时 new UserToken(loginCode, password, LoginType.NORMAL.getDesc()); 和 new UserToken(loginCode, password, LoginType.FREE.getDesc()); 依然还是遍历在查找realm,而不是去指定的realm中进行登录匹配 最后导致错误的账号和密码会返回 AuthenticationException: Authentication token of type [class xyz.murasakichigo.demo.shiro.UserToken] could not be authenticated by any configured realms. Please ensure that at least one realm can authenticate these tokens. 无法抓取UnknownAccountException和IncorrectCredentialsException这两个异常 虽然说问题并不大...但是比较好奇。 总结一下,怎么指定subject.login(token)时去指定的realm进行登录匹配,而不是挨个realm遍历 最后,正常密码登录的示例中 UserToken token = new UserToken(loginName, loginPwd, LoginType.FREE.getDesc()); 此处的枚举应该是NORMAL吧?