认证
认证流程
- 获取当前的Subject,调用SecurityUtils.getSubject();
- 测试当前用户是否已经被认证了,即是否已经登录,调用Subject的isAythenticated()判断是否认证
- 如果没有认证,则把用户的账户和密码封装到UsernamePasswordToken对象
- 执行登录:调用Subject的login(AuthenticationToken)方法
- 自定义Realm,从数据库中获取对应记录,返回给shiro
- 实际上需要继承org.apache.shiro.realm.AuthenticatingRealm类
- 实现doGetAuthenticationInfo(AuthenticationToken)方法
- 由shiro完成密码的比对
代码认证
接收表单请求,进行认证
@PostMapping("/login") public String login(String name, String password) { // 获取Subject Subject currentUser = SecurityUtils.getSubject(); if (!currentUser.isAuthenticated()) { // 没有认证 UsernamePasswordToken token = new UsernamePasswordToken(name, password); token.setRememberMe(true); try { // 执行登录 currentUser.login(token); } catch (AuthenticationException ae) { System.err.println("登录失败: " + ae.getMessage()); } } return "index"; }
自定义Realm类,继承AuthenticatingRealm,重写doGetAuthenticationInfo()方法
步骤:
- 把AuthenticationToken转换成为 UsernamePasswordToken
- 从UsernamePasswordToken 中来获取username
- 调用数据库的方法,从数据库中查询username 对应的用户记录
- 若用户不存在,则可以抛出UnknownAccountException
- 根据用户信息的情况,决定是否需要抛出其他异常,如用户被锁定等
- 根据用户的情况,来构建AuthenticationInfo对象并返回,通常使用SimpleAuthenticationInfo实现类
public class MyShiroRealm extends AuthenticatingRealm { @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { // 1. 把AuthenticationToken转换成为 UsernamePasswordToken UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; // 2. 从UsernamePasswordToken 中来获取username String username = token.getUsername(); // 3. 调用数据库的方法,从数据库中查询username 对应的用户记录 System.out.printf("从数据库中获取%s相关信息", username); // 4. 若用户不存在,则可以抛出UnknownAccountException异常 // 5. 根据用户信息的情况,决定是否需要抛出其他异常,如用户被锁定等 switch (username) { case "unknown": throw new UnknownAccountException("用户不存在!"); case "monster": throw new LockedAccountException("用户被锁定"); } // 6. 根据用户的情况,来构建AuthenticationInfo对象并返回,通常使用SimpleAuthenticationInfo实现类 // 以下信息是从数据库中获取的 // 1)principal: 认证的实体信息,可以是username,也可以是数据表对应的用户的实体类对象 Object principal = username; // 2) credentials: 数据表中获取的密码 Object credentals = "123456"; // 3) realmName: 当前realm对象的name, 调用父类的getName()方法即可 String realmName = getName(); return new SimpleAuthenticationInfo(principal, credentals, realmName); } }
在配置类中将自定义的realm添加到容器
//将自己的验证方式加入容器 @Bean public MyShiroRealm myShiroRealm() { return new MyShiroRealm(); } //权限管理,配置主要是Realm的管理认证 @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(myShiroRealm()); return securityManager; }
登出
登出操作非常简单,shiro已经做好了,只需要在配置中配置登出过滤器即可
// 登出,具体的代码shiro已经实现了 map.put("/logout","logout");
要登出的时候访问/logout即可
密码比对
密码比对是通过AuthenticatingRealm 的 credentialsMatcher属性来进行密码的比对
那么问题是我们肯定不能你拿明文进行比对,因此我们需要进行加密
我们使用HashedCredentialsMatcher 对象,并设置加密算法即可
在shiro的配置类中添加这样一段代码
/** * HashedCredentialsMatcher类是对密码进行加密的,保证保存在数据库中的密码是密文 * 当然在登录认证的时候也可以对表单中传入的数据进行加密 * @return */ @Bean("hashedCredentialsMatcher") public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); // 指定加密方式 hashedCredentialsMatcher.setHashAlgorithmName("MD5"); // 指定加密次数 hashedCredentialsMatcher.setHashIterations(1024); hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true); return hashedCredentialsMatcher; } //将自己的验证方式加入容器
// 设置自定义的Realm的CredentialsMatcher属性
@Bean public MyShiroRealm myShiroRealm(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher matcher) { MyShiroRealm myShiroRealm = new MyShiroRealm(); myShiroRealm.setAuthenticationCachingEnabled(false); myShiroRealm.setCredentialsMatcher(matcher); return myShiroRealm; } //权限管理,配置主要是Realm的管理认证 @Bean public SecurityManager securityManager(@Qualifier("myShiroRealm") MyShiroRealm myShiroRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(myShiroRealm); return securityManager; }
MD5盐值加密
如何加盐?
- 在doGetAuthenticationInfo()方法返回值创建SimpleAuthenticationInfo 对象的时候,需要使用new SimpleAuthenticationInfo(principal, credentals, credentialsSalt, realmName);
- 使用ByteSource.Util.bytes();来计算盐值
- 盐值需要唯一,一般使用随机字符串user id
- 使用new SimpleHash("MD5", "123456", credentialsSalt, 1024);获取加密加盐后的密码
@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { // 1. 把AuthenticationToken转换成为 UsernamePasswordToken UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; // 2. 从UsernamePasswordToken 中来获取username String username = token.getUsername(); // 3. 调用数据库的方法,从数据库中查询username 对应的用户记录 System.out.printf("从数据库中获取%s相关信息", username); // 4. 若用户不存在,则可以抛出UnknownAccountException异常 // 5. 根据用户信息的情况,决定是否需要抛出其他异常,如用户被锁定等 switch (username) { case "unknown": throw new UnknownAccountException("用户不存在!"); case "monster": throw new LockedAccountException("用户被锁定"); } // 6. 根据用户的情况,来构建AuthenticationInfo对象并返回,通常使用SimpleAuthenticationInfo实现类 // 以下信息是从数据库中获取的 // 1)principal: 认证的实体信息,可以是username,也可以是数据表对应的用户的实体类对象 Object principal = username; // 2) credentials: 数据表中获取的密码 Object credentals = ""; if ("admin".equals(username)) { credentals = "038bdaf98f2037b31f1e75b5b4c9b26e"; } else if ("admin1".equals(username)) { credentals = "5826c6b4f4f7b527b3d6ecd6f45401a9"; } // 3) credentialsSalt: 加盐,一般用user id ByteSource credentialsSalt = ByteSource.Util.bytes(username); // 4) realmName: 当前realm对象的name, 调用父类的getName()方法即可 String realmName = getName(); return new SimpleAuthenticationInfo(principal, credentals, credentialsSalt, realmName); }
多Realm
多个Realm需要将多个自定义的Realm注入到容器,然后在securityManager方法中将自定义的Realm添加
@Bean public MyShiroRealm myShiroRealm(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher matcher) { MyShiroRealm myShiroRealm = new MyShiroRealm(); myShiroRealm.setAuthenticationCachingEnabled(false); myShiroRealm.setCredentialsMatcher(matcher); return myShiroRealm; } @Bean public SecondShiroRealm secondShiroRealm() { return new SecondShiroRealm(); }
将Realm添加,注意使用securityManager.setRealms()将多个Realm的列表添加进去
@Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealms(Arrays.asList(myShiroRealm(), secondShiroRealm())); return securityManager; }
认证策略AuthenticationStrategy
AuthenticationStrategy接口的默认实现
- FirstSuccessfulStrategy:只要有一个Realm验证成功即可,只返回第一个Realm身份验证成功的认证信息,其他忽略
- AtLeastOneSuccessfulStrategy:只要有一个Realm验证成功即可,和FirstSuccessfulStrategy不同,这个会将所有的Realm身份验证成功的认证信息返回。也是默认值
- AllSuccessfulStrategy:所有的Realm验证成功才算成功,且返回所有Realm身份验证成功的信息,如果有一个失败就算失败了
配置认证策略
在配置类中配置如下信息,然后将配置信息添加到SecurityManager中即可
@Bean public ModularRealmAuthenticator modularRealmAuthenticator(){ ModularRealmAuthenticator modularRealmAuthenticator = new ModularRealmAuthenticator(); modularRealmAuthenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy()); return modularRealmAuthenticator; } @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 将自定义的管理Realm的规则添加 securityManager.setAuthenticator(modularRealmAuthenticator()); return securityManager; }
自定义UserModularRealmAuthenticator实现管理多Realm
上面的多个Realm在登录验证的时候每个都会进行验证,然后通过modularRealmAuthenticator()里面我们自己配置的规则来判断是全部验证通过才通过还是只要一个Realm验证通过就通过。
但是我们能不能精确的指定让哪一个Realm验证呢,自然可以,只需要自定义UserModularRealmAuthenticator类,继承ModularRealmAuthenticator类,然后重写doAuthenticate()方法,在方法里面指定验证规则即可
那么接下来开始自定义:
首先定义一个枚举类型用来选择要走的Realm
import lombok.Getter; import org.apache.shiro.realm.Realm; @Getter public enum LoginType { Admin(2, "Admin"), Student(0, "Student"), Teacher(1, "Teacher"); LoginType(Integer code, String msg) { this.code = code; this.msg = msg; } private Integer code; private String msg; /** * 判断传入的Realm是否参与本次验证 * @param realm * @return */ public boolean isThisRealm(Realm realm) { // 获取自定义的注解 Class<RealmType> annotationClass = RealmType.class; // 判断这个类是否标注了自定义注解 if (realm.getClass().isAnnotationPresent(annotationClass)) { // 获取传入的类标注的注解 RealmType annotation = realm.getClass().getAnnotation(annotationClass); // 获取注解上的信息 int code = annotation.code(); // 判断注解里面的code是否与当前枚举类型的code相同 return this.getCode() == code; } return false; } }
枚举类型里面有一个isThisRealm()方法,是用于判断传入的Realm是否是我们想要执行的Realm的,因为后面会自定义一个注解,标注在每个Realm上,如果注解上面的code值跟当前枚举类型的code相等,就表示是我们想要走的Realm,就返回true,在自定义的UserModularRealmAuthenticator中就会把这个Realm放到要执行的Realm列表里面
自定义UserToken
自定义的UserToken要继承UsernamePasswordToken类,UserToken类里需要添加一个LoginType字段去选择要走的Realm
import lombok.Data; import org.apache.shiro.authc.UsernamePasswordToken; @Data public class UserToken extends UsernamePasswordToken { private LoginType loginType; public UserToken() { } public UserToken(String username, String password, LoginType loginType) { super(username, password); this.loginType = loginType; } }
自定义UserModularRealmAuthenticator
自定义UserModularRealmAuthenticator,继承ModularRealmAuthenticator,在doAuthenticate()方法中筛选出我们要执行的Realm
import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.pam.ModularRealmAuthenticator; import org.apache.shiro.realm.Realm; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; public class UserModularRealmAuthenticator extends ModularRealmAuthenticator { /** * 重写doAuthenticate * 判断是单Realm还是多Realm,分别去不同的方法执行 * @param authenticationToken * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { // 判断getRealms()是否为空 assertRealmsConfigured(); // 强制转换成自定义的UserToken UserToken userToken = (UserToken) authenticationToken; // 获取验证的类型 LoginType loginType = userToken.getLoginType(); // 获取所有的Realm Collection<Realm> realms = getRealms(); // 筛选出与loginType的code相同的Realm List<Realm> collect = realms.parallelStream() .filter(loginType::isThisRealm) .collect(Collectors.toList()); // 判断是单Realm还是多Realm,分别执行不同的方法 return collect.size() == 1 ? this.doSingleRealmAuthentication(collect.iterator().next(), userToken) : this.doMultiRealmAuthentication(collect, userToken); } }
在配置类中将我们自定义的UserModularRealmAuthenticator添加到容器中
/** * 系统自带的Realm管理,主要针对多realm * */ @Bean public ModularRealmAuthenticator modularRealmAuthenticator(){ UserModularRealmAuthenticator modularRealmAuthenticator = new UserModularRealmAuthenticator(); modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy()); return modularRealmAuthenticator; }
注意:别忘记了在securityManager()中使用securityManager.setAuthenticator(modularRealmAuthenticator());将自定义的管理Realm的规则添加进去
自定义注解,指定code,对应枚举类里面的code
import java.lang.annotation.*; @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface RealmType { int code() default 0; }
在对应的Realm类上标注上自定义的注解
// 第一个Realm @RealmType public class SecondShiroRealm extends AuthenticatingRealm { // 第二个Realm @RealmType(code = 1) public class MyShiroRealm extends AuthenticatingRealm {
使用
最后在controller中初始化UserToken对象,指定LoginType,然后执行login()方法
// 获取Subject Subject currentUser = SecurityUtils.getSubject(); UserToken token = new UserToken(name, password, LoginType.Student); // 执行登录 currentUser.login(token);
通过如上配置就可以在自己指定要执行的Realm,精准的控制粒度