1. 程式人生 > >Shiro之使用者名稱密碼或手機號簡訊登入(多realm認證)

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 (!password.equals(user.getPassword())) {
            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替換預設的即可。