在登录认证中,经常需要实现用户名密码和手机号验证码这两种登录方式。
最近学了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替换默认的即可。