Shiro之用户名密码或手机号短信登录(多realm认证)

  • A+
所属分类:java

在登录认证中,我们经常遇到需要实现用户名密码和手机号验证码这两种登录方式,如下图所示:

Shiro之用户名密码或手机号短信登录(多realm认证)

小编最近学了Shiro,所以在这里记录下。

用户名密码使用的token自然是UsernamePasswordToken,我们可以参考UsernamePasswordToken,自定义PhoneToken,在不同的控制器中传入Token,然后由Realm判断当前的Token属于UsernamePasswordToken还是PhoneToken。

自定义Token:

  1. public class PhoneToken implements HostAuthenticationToken, RememberMeAuthenticationToken, Serializable {
  2.     // 手机号码
  3.     private String phone;
  4.     private boolean rememberMe;
  5.     private String host;
  6.     /**
  7.      * 重写getPrincipal方法
  8.      */
  9.     public Object getPrincipal() {
  10.         return phone;
  11.     }
  12.     /**
  13.      * 重写getCredentials方法
  14.      */
  15.     public Object getCredentials() {
  16.         return phone;
  17.     }
  18.     public PhoneToken() { this.rememberMe = false; }
  19.     public PhoneToken(String phone) { this(phone, falsenull); }
  20.     public PhoneToken(String phone, boolean rememberMe) { this(phone, rememberMe, null); }
  21.     public PhoneToken(String phone, boolean rememberMe, String host) {
  22.         this.phone = phone;
  23.         this.rememberMe = rememberMe;
  24.         this.host = host;
  25.     }
  26.     public String getPhone() {
  27.         return phone;
  28.     }
  29.     public void setPhone(String phone) {
  30.         this.phone = phone;
  31.     }
  32.     @Override
  33.     public String getHost() {
  34.         return host;
  35.     }
  36.     @Override
  37.     public boolean isRememberMe() {
  38.         return rememberMe;
  39.     }
  40. }

自定义Realm:

  1. public class UserRealm extends AuthorizingRealm {
  2.     @Autowired
  3.     private UserService userService;
  4.     // 认证
  5.     @Override
  6.     protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
  7.         UsernamePasswordToken token = null;
  8.         if(authenticationToken instanceof UsernamePasswordToken){
  9.             token = (UsernamePasswordToken) authenticationToken;
  10.         }else{
  11.             return null;
  12.         }
  13.         String username = token.getUsername();
  14.         if(StringUtils.isBlank(username)){
  15.             return null;
  16.         }
  17.         UserDO user = userService.getUser(token.getUsername());
  18.         // 账号不存在
  19.         if (user == null) {
  20.             throw new CustomAuthenticationException("账号或密码不正确");
  21.         }
  22.         // 账号锁定
  23.         if (user.getStatus() == 1) {
  24.             throw new CustomAuthenticationException("账号已被锁定,请联系管理员");
  25.         }
  26.         // 主体,一般存用户名或用户实例对象,用于在其他地方获取当前认证用户信息
  27.         Object principal = user;
  28.         // 凭证,这里是从数据库取出的加密后的密码,Shiro会用于与token中的密码比对
  29.         Object hashedCredentials = user.getPassword();
  30.         // 以用户名作为盐值
  31.         ByteSource credentialsSalt = ByteSource.Util.bytes(token.getUsername());
  32.         return new SimpleAuthenticationInfo(principal, hashedCredentials, credentialsSalt, this.getName());
  33.     }
  34.     // 授权
  35.     @Override
  36.     protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
  37.         return null;
  38.     }
  39.     @Override
  40.     public boolean supports(AuthenticationToken var1){
  41.         return var1 instanceof UsernamePasswordToken;
  42.     }
  43. }
  1. public class PhoneRealm extends AuthorizingRealm {
  2.     @Resource
  3.     UserService userService;
  4.     // 认证
  5.     @Override
  6.     protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
  7.         PhoneToken token = null;
  8.         // 如果是PhoneToken,则强转,获取phone;否则不处理。
  9.         if(authenticationToken instanceof PhoneToken){
  10.             token = (PhoneToken) authenticationToken;
  11.         }else{
  12.             return null;
  13.         }
  14.         String phone = (String) token.getPrincipal();
  15.         UserDO user = userService.selectByPhone(phone);
  16.         if (user == null) {
  17.             throw new CustomAuthenticationException("手机号错误");
  18.         }
  19.         return new SimpleAuthenticationInfo(user, phone, this.getName());
  20.     }
  21.     // 授权
  22.     @Override
  23.     protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
  24.         return null;
  25.     }
  26.     @Override
  27.     public boolean supports(AuthenticationToken var1){
  28.         return var1 instanceof PhoneToken;
  29.     }
  30. }

控制器中的使用:

  1. @RequestMapping("/user")
  2. @Controller
  3. public class UserController {
  4.     // 用户名密码登录
  5.     @PostMapping("/dologin")
  6.     @ResponseBody
  7.     public BackAdminResult dologin(@RequestParam("username") String username, @RequestParam("password") String password, HttpSession session) throws AuthenticationException {
  8.         UsernamePasswordToken token = new UsernamePasswordToken(username, password);
  9.         Subject subject = SecurityUtils.getSubject();
  10.         subject.login(token);
  11.         UserDO user = (UserDO) subject.getPrincipal();
  12.         session.setAttribute(Constants.LOGIN_ADMIN_KEY, user);
  13.         subject.getSession().setAttribute(Constants.LOGIN_ADMIN_KEY, user);
  14.         return BackAdminResult.build(0"登录成功!");
  15.     }
  16.     // 使用手机号和短信验证码登录
  17.     @RequestMapping("/plogin")
  18.     @ResponseBody
  19.     public BackAdminResult pLogin(@RequestParam("phone") String phone, @RequestParam("code") String code, HttpSession session){
  20.         // 根据phone从session中取出发送的短信验证码,并与用户输入的验证码比较
  21.         String messageCode = (String) session.getAttribute(phone);
  22.         if(StringUtils.isNoneBlank(messageCode) && messageCode.equals(code)){
  23.             UserNamePasswordPhoneToken token = new UserNamePasswordPhoneToken(phone);
  24.             Subject subject = SecurityUtils.getSubject();
  25.             subject.login(token);
  26.             UserDO user = (UserDO) subject.getPrincipal();
  27.             session.setAttribute(Constants.LOGIN_ADMIN_KEY, user);
  28.             return BackAdminResult.build(0"登录成功!");
  29.         }else{
  30.             return BackAdminResult.build(2"验证码错误!");
  31.         }
  32.     }
  33. }

配置(部分):

  1. @Configuration
  2. public class ShiroConfig {
  3.     /**
  4.      * 加密策略
  5.      */
  6.     @Bean
  7.     public CredentialsMatcher credentialsMatcher(){
  8.         HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
  9.         // 加密算法:MD5、SHA1
  10.         credentialsMatcher.setHashAlgorithmName(Constants.Hash_Algorithm_Name);
  11.         // 散列次数
  12.         credentialsMatcher.setHashIterations(Constants.Hash_Iterations);
  13.         return credentialsMatcher;
  14.     }
  15.     /**
  16.      * 自定义Realm
  17.      */
  18.     @Bean
  19.     public UserRealm userRealm(CredentialsMatcher credentialsMatcher) {
  20.         UserRealm userRealm = new UserRealm();
  21.         userRealm.setCredentialsMatcher(credentialsMatcher);
  22.         userRealm.setCacheManager(shiroCacheManager());
  23.         return userRealm;
  24.     }
  25.     @Bean
  26.     public PhoneRealm phoneRealm(){
  27.         PhoneRealm phoneRealm = new PhoneRealm();
  28.         phoneRealm.setCacheManager(shiroCacheManager());
  29.         return phoneRealm;
  30.     }
  31.     /**
  32.      * 认证器
  33.      */
  34.     @Bean
  35.     public AbstractAuthenticator abstractAuthenticator(UserRealm userRealm, PhoneRealm phoneRealm){
  36.         // 自定义模块化认证器,用于解决多realm抛出异常问题
  37.         ModularRealmAuthenticator authenticator = new CustomModularRealmAuthenticator();
  38.         // 认证策略:AtLeastOneSuccessfulStrategy(默认),AllSuccessfulStrategy,FirstSuccessfulStrategy
  39.         authenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
  40.         // 加入realms
  41.         List<Realm> realms = new ArrayList<>();
  42.         realms.add(userRealm);
  43.         realms.add(phoneRealm);
  44.         authenticator.setRealms(realms);
  45.         return authenticator;
  46.     }
  47.     @Bean
  48.     public SecurityManager securityManager(UserRealm userRealm, PhoneRealm phoneRealm, AbstractAuthenticator abstractAuthenticator) {
  49.         DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
  50.         // 设置realms
  51.         List<Realm> realms = new ArrayList<>();
  52.         realms.add(userRealm);
  53.         realms.add(phoneRealm);
  54.         securityManager.setRealms(realms);
  55.         // 自定义缓存实现,可以使用redis
  56.         securityManager.setCacheManager(shiroCacheManager());
  57.         // 自定义session管理,可以使用redis
  58.         securityManager.setSessionManager(sessionManager());
  59.         // 注入记住我管理器
  60.         securityManager.setRememberMeManager(rememberMeManager());
  61.         // 认证器
  62.         securityManager.setAuthenticator(abstractAuthenticator);
  63.         return securityManager;
  64.     }
  65. }

自定义异常:

  1. public class CustomAuthenticationException extends AuthenticationException {
  2.     // 异常信息
  3.     private String msg;
  4.     public CustomAuthenticationException(String msg){
  5.         super(msg);
  6.         this.msg = msg;
  7.     }
  8.     public String getMsg() {
  9.         return msg;
  10.     }
  11.     public void setMsg(String msg) {
  12.         this.msg = msg;
  13.     }
  14. }

自定义异常处理:

  1. /**
  2.  * 用于捕获和处理Controller抛出的异常
  3.  */
  4. @ControllerAdvice
  5. public class GlobalExceptionHandler {
  6.     private final static Logger LOG = LoggerFactory.getLogger(GlobalExceptionHandler.class);
  7.     @ExceptionHandler(CustomAuthenticationException.class)
  8.     @ResponseBody
  9.     public BackAdminResult handleAuthentication(Exception ex){
  10.         LOG.info("Authentication Exception handler  " + ex.getMessage() );
  11.         return BackAdminResult.build(1, ex.getMessage());
  12.     }
  13. }

这里有个问题,就是默认的ModularRealmAuthenticator在处理多realm时,会把异常捕获,导致自定义异常处理器捕获不到认证时抛出的异常,所以需要重写ModularRealmAuthenticator的doMultiRealmAuthentication方法,把异常抛出来。

  1. public class CustomModularRealmAuthenticator extends ModularRealmAuthenticator {
  2.     /**
  3.      * 重写doMultiRealmAuthentication,抛出异常,便于自定义ExceptionHandler捕获
  4.      */
  5.     @Override
  6.     public AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) throws AuthenticationException {
  7.         AuthenticationStrategy strategy = this.getAuthenticationStrategy();
  8.         AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
  9.         Iterator var5 = realms.iterator();
  10.         while(var5.hasNext()) {
  11.             Realm realm = (Realm)var5.next();
  12.             aggregate = strategy.beforeAttempt(realm, token, aggregate);
  13.             if (realm.supports(token)) {
  14.                 AuthenticationInfo info = null;
  15.                 Throwable t = null;
  16.                 info = realm.getAuthenticationInfo(token);
  17.                 aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);
  18.             }
  19.         }
  20.         aggregate = strategy.afterAllAttempts(token, aggregate);
  21.         return aggregate;
  22.     }
  23. }

在配置那里把自定义的ModularRealmAuthenticator替换默认的即可。

avatar

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: