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

这里写图片描述
在登录认证中,经常需要实现用户名密码和手机号验证码这两种登录方式。

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

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

自定义Token:

public class PhoneToken implements HostAuthenticationToken, RememberMeAuthenticationToken, Serializable {

    // 手机号码
    private String phone;
    private boolean rememberMe;
    private String host;

    /**
     * 重写getPrincipal方法
     */
    public Object getPrincipal() {
        return phone;
    }

    /**
     * 重写getCredentials方法
     */
    public Object getCredentials() {
        return phone;
    }

    public PhoneToken() { this.rememberMe = false; }

    public PhoneToken(String phone) { this(phone, false, null); }

    public PhoneToken(String phone, boolean rememberMe) { this(phone, rememberMe, null); }

    public PhoneToken(String phone, boolean rememberMe, String host) {
        this.phone = phone;
        this.rememberMe = rememberMe;
        this.host = host;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    @Override
    public String getHost() {
        return host;
    }

    @Override
    public boolean isRememberMe() {
        return rememberMe;
    }
}

自定义Realm:

public class UserRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;
    
    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {

        UsernamePasswordToken token = null;

        if(authenticationToken instanceof UsernamePasswordToken){
            token = (UsernamePasswordToken) authenticationToken;
        }else{
            return null;
        }

        String username = token.getUsername();

        if(StringUtils.isBlank(username)){
            return null;
        }

        UserDO user = userService.getUser(token.getUsername());

        // 账号不存在
        if (user == null) {
            throw new CustomAuthenticationException("账号或密码不正确");
        }

        // 账号锁定
        if (user.getStatus() == 1) {
            throw new CustomAuthenticationException("账号已被锁定,请联系管理员");
        }

        // 主体,一般存用户名或用户实例对象,用于在其他地方获取当前认证用户信息
        Object principal = user;
        // 凭证,这里是从数据库取出的加密后的密码,Shiro会用于与token中的密码比对
        Object hashedCredentials = user.getPassword();
        // 以用户名作为盐值
        ByteSource credentialsSalt = ByteSource.Util.bytes(token.getUsername());
        
        return new SimpleAuthenticationInfo(principal, hashedCredentials, credentialsSalt, this.getName());
    }
    
    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }
    
    @Override
    public boolean supports(AuthenticationToken var1){
        return var1 instanceof UsernamePasswordToken;
    }
}
public class PhoneRealm extends AuthorizingRealm {

    @Resource
    UserService userService;

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {

        PhoneToken token = null;

        // 如果是PhoneToken,则强转,获取phone;否则不处理。
        if(authenticationToken instanceof PhoneToken){
            token = (PhoneToken) authenticationToken;
        }else{
            return null;
        }

        String phone = (String) token.getPrincipal();

        UserDO user = userService.selectByPhone(phone);

        if (user == null) {
            throw new CustomAuthenticationException("手机号错误");
        }
        return new SimpleAuthenticationInfo(user, phone, this.getName());
    }

    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }
    
    @Override
    public boolean supports(AuthenticationToken var1){
        return var1 instanceof PhoneToken;
    }
}

控制器中的使用:

@RequestMapping("/user")
@Controller
public class UserController {

    // 用户名密码登录
    @PostMapping("/dologin")
    @ResponseBody
    public BackAdminResult dologin(@RequestParam("username") String username, @RequestParam("password") String password, HttpSession session) throws AuthenticationException {

        UsernamePasswordToken token = new UsernamePasswordToken(username, password);

        Subject subject = SecurityUtils.getSubject();

        subject.login(token);
        UserDO user = (UserDO) subject.getPrincipal();
        session.setAttribute(Constants.LOGIN_ADMIN_KEY, user);
        subject.getSession().setAttribute(Constants.LOGIN_ADMIN_KEY, user);
        return BackAdminResult.build(0, "登录成功!");

    }

    // 使用手机号和短信验证码登录
    @RequestMapping("/plogin")
    @ResponseBody
    public BackAdminResult pLogin(@RequestParam("phone") String phone, @RequestParam("code") String code, HttpSession session){

        // 根据phone从session中取出发送的短信验证码,并与用户输入的验证码比较
        String messageCode = (String) session.getAttribute(phone);

        if(StringUtils.isNoneBlank(messageCode) && messageCode.equals(code)){

            UserNamePasswordPhoneToken token = new UserNamePasswordPhoneToken(phone);

            Subject subject = SecurityUtils.getSubject();

            subject.login(token);
            UserDO user = (UserDO) subject.getPrincipal();
            session.setAttribute(Constants.LOGIN_ADMIN_KEY, user);
            return BackAdminResult.build(0, "登录成功!");

        }else{
            return BackAdminResult.build(2, "验证码错误!");
        }
    }
}

配置(部分):

@Configuration
public class ShiroConfig {
    /**
     * 加密策略
     */
    @Bean
    public CredentialsMatcher credentialsMatcher(){
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        // 加密算法:MD5、SHA1
        credentialsMatcher.setHashAlgorithmName(Constants.Hash_Algorithm_Name);
        // 散列次数
        credentialsMatcher.setHashIterations(Constants.Hash_Iterations);
        return credentialsMatcher;
    }
    /**
     * 自定义Realm
     */
    @Bean
    public UserRealm userRealm(CredentialsMatcher credentialsMatcher) {
        UserRealm userRealm = new UserRealm();
        userRealm.setCredentialsMatcher(credentialsMatcher);
        userRealm.setCacheManager(shiroCacheManager());
        return userRealm;
    }
    @Bean
    public PhoneRealm phoneRealm(){
        PhoneRealm phoneRealm = new PhoneRealm();
        phoneRealm.setCacheManager(shiroCacheManager());
        return phoneRealm;
    }

    /**
     * 认证器
     */
    @Bean
    public AbstractAuthenticator abstractAuthenticator(UserRealm userRealm, PhoneRealm phoneRealm){
        // 自定义模块化认证器,用于解决多realm抛出异常问题
        ModularRealmAuthenticator authenticator = new CustomModularRealmAuthenticator();
        // 认证策略:AtLeastOneSuccessfulStrategy(默认),AllSuccessfulStrategy,FirstSuccessfulStrategy
        authenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
        // 加入realms
        List<Realm> realms = new ArrayList<>();
        realms.add(userRealm);
        realms.add(phoneRealm);
        authenticator.setRealms(realms);
        return authenticator;
    }

    @Bean
    public SecurityManager securityManager(UserRealm userRealm, PhoneRealm phoneRealm, AbstractAuthenticator abstractAuthenticator) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 设置realms
        List<Realm> realms = new ArrayList<>();
        realms.add(userRealm);
        realms.add(phoneRealm);
        securityManager.setRealms(realms);
        // 自定义缓存实现,可以使用redis
        securityManager.setCacheManager(shiroCacheManager());
        // 自定义session管理,可以使用redis
        securityManager.setSessionManager(sessionManager());
        // 注入记住我管理器
        securityManager.setRememberMeManager(rememberMeManager());
        // 认证器
        securityManager.setAuthenticator(abstractAuthenticator);
        return securityManager;
    }
}

自定义异常:

public class CustomAuthenticationException extends AuthenticationException {

    // 异常信息
    private String msg;

    public CustomAuthenticationException(String msg){
        super(msg);
        this.msg = msg;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }
}

自定义异常处理:

/**
 * 用于捕获和处理Controller抛出的异常
 */
@ControllerAdvice
public class GlobalExceptionHandler {

    private final static Logger LOG = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(CustomAuthenticationException.class)
    @ResponseBody
    public BackAdminResult handleAuthentication(Exception ex){
        LOG.info("Authentication Exception handler  " + ex.getMessage() );
        return BackAdminResult.build(1, ex.getMessage());
    }
}

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

public class CustomModularRealmAuthenticator extends ModularRealmAuthenticator {

    /**
     * 重写doMultiRealmAuthentication,抛出异常,便于自定义ExceptionHandler捕获
     */
    @Override
    public AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) throws AuthenticationException {

        AuthenticationStrategy strategy = this.getAuthenticationStrategy();
        AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);

        Iterator var5 = realms.iterator();

        while(var5.hasNext()) {
            Realm realm = (Realm)var5.next();
            aggregate = strategy.beforeAttempt(realm, token, aggregate);
            if (realm.supports(token)) {

                AuthenticationInfo info = null;
                Throwable t = null;

                info = realm.getAuthenticationInfo(token);

                aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);
            }
        }
        aggregate = strategy.afterAllAttempts(token, aggregate);
        return aggregate;
    }
}

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

展开阅读全文

没有更多推荐了,返回首页