Shiro之使用者名稱密碼或手機號簡訊登入(多realm認證)
阿新 • • 發佈:2018-11-27
在登入認證中,經常需要實現使用者名稱密碼和手機號驗證碼這兩種登入方式。
最近學了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替換預設的即可。