背景:最近在把公司之前的项目架构升级到SpringBoot,同时因为之前项目需要为每个人到开一个账号有点麻烦,所以这次升级的时候加入了LDAP,让公司的人通过OA账号就能登录,同时保留原有的登录,让没有OA账号的能够走自己的登录
关于LDAP可以去看我写的LDAP的博客,里面简单的介绍了什么是LDAP,Shiro的话可以去网上看看教程,后期可能会写一篇教程。
Shiro的登录流程
shiro的登录流程简单来说有三步:
- 根据usernam和password生成UsernamePasswordToken
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
- 使用生成的token进行登录
Subject subject = SecurityUtils.getSubject();//生成subject
subject.login(token);
- 实现自定义的Realm,重写doGetAuthenticationInfo方法,doGetAuthenticationInfo方法里面写我们自己的登录过程
public class UserRealm extends AuthorizingRealm{
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token){
//实现自己的登录流程
}
}
如果只是简单的登录,实现上面三个步骤即可,但是因为我们接入了LDAP的登录,所以我们这里需要对登录类型进行判断,是LDAP的登录走LDAP,不是的走自己的登录。所以我们需要继承UsernamePasswordToken,加入一个变量loginType,来标志登录类型
@Getter
public class CustomToken extends UsernamePasswordToken {
private Integer loginType;//1为LDAP登录
public CustomToken(String username, String password, Integer loginType) {
super(username, password);
this.loginType = loginType;
}
}
我们使用CustomToken来生成token,在创建token的时候除了传入username和password还有传入loginType。
CustomTokentoken cToken = new CustomToken(username, password,loginType);
登录还是一样
subject.login(cToken)
在我们实现的doGetAuthenticationInfo方法,需要做一些改变。先把token转为我们自己的CustomTokentoken,然后去除logintype,判断logintype是否是1,是1的话走LDAP,这里因为公司有提供访问的接口,不用搭LDAP服务了
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
CustomTokentoken cToken= (CustomTokentoken ) token;
int loginType = cToken.getLoginType();
String username = (String) cToken.getPrincipal();
String password = String.valueOf(cToken.getPassword());
// 走LDAP
if (loginType == 1){
log.info("走的LDAP登录: [username: "+ username +"]");
//下面是跟我们的业务有关,不需要写
String ssoUser = SsoRestful.authenticateSSOUser(username, password);
String[] splitSsoUser = ssoUser.split(",");
String cn = splitSsoUser[splitSsoUser.length - 2];
String[] cnSplit = cn.split(":");
String realmName = cnSplit[1];
String[] splitUser = splitSsoUser[0].substring(1).split(":");
//上面的可以省略
password = new SimpleHash("md5",password,ByteSource.Util.bytes(username),2).toHex();
if (("\"true\"").equals(splitUser[1])){
return new SimpleAuthenticationInfo(username,password,ByteSource.Util.bytes(username),realmName);
}else {
log.error("username[" + username
+ "] or password error");
throw new UnknownAccountException("username[" + username
+ "] error");
}
}
//走自己的用户登录
User user = userService.findByName(username, null);
if (user == null) {
log.error("username[" + username
+ "] login error, there is no user");
throw new UnknownAccountException("username[" + username
+ "] login error, there is no user");
}
if ("0".equals(user.getIsOpen())) {
log.info("the current user[" + username
+ "] has been closed, can not login.");
return null;
}
// 交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配,如果觉得人家的不好可以自定义实现
return new SimpleAuthenticationInfo(
// 用户名
user.getUserName(),
// 密码
user.getPassword(),
// salt=username+salt
ByteSource.Util.bytes(user.getCredentialsSalt()),
// realm name
user.getRealName()
);
}
还有一点需要注意的是,从源码中可知,登录结束后,shiro会进行密码匹配,去校验你输入的密码是否正确。
Shiro的密码匹配有好几种,有简单匹配的,就是密码明文匹配,也有密码加密后匹配的,这里我们用的是密码加密后匹配。
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");
hashedCredentialsMatcher.setHashIterations(2);
hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
return hashedCredentialsMatcher;
}
因此我们在走LDAP登录的时候,当登录成功后,需要手动的把密码,按照上面的规则就行加密再返回,否则会出现密码匹配失败的错误
password = new SimpleHash("md5",password,ByteSource.Util.bytes(username),2).toHex();//加密算法是MD5,salt是username,加密两次
if (("\"true\"").equals(splitUser[1])){
return new SimpleAuthenticationInfo(username,password,ByteSource.Util.bytes(username),realmName);
走自己的登录就不需要改动什么,跟之前的一样即可