文章目录
0、场景介绍
场景:假设在我们的系统中 有两个角色 用户 和 管理员,用户有100W,管理员只有 2个
如果设计到一张表 ,那么查询管理员的时候,最坏情况需要遍历 100W+ 次,所以适合设置在两张表中。
现在的管理员登陆:通过用户名查询 用户数据(100W+)
如果是我们能够将这两个角色的数据设计到两张表里面
管理员的表,用户的表 假设我们都使用shiro来进行认证 ? 怎么办? 设计思路?
就是在用户登陆时候 设置一个 登陆类型
重写 UserNameAndPasswordToken 在执行正式的登陆的时候,根据登陆的类型选择 我们要使用的Realm, 最终去进行认证 那么就可以实现这个功能
存在两个问题
1:在哪里去做选择 执行哪一个realm?
思路:ModularRealmAuthenticator.java类进行拓展(直接和realm打交道的)
2:我们的登陆类型如何来定
思路:可以在控制器中确定
1、基本环境搭建
1.1、导入依赖
<dependencies>
<!-- druid连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
<!-- SpringBoot和shiro的整合 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
1.2、配置 application.properties
server.port=8080
spring.profiles.active=dev
spring.application.name=springboot
# mybatis配置
mybatis.type-aliases-package=com.fu.shiro.pojo
mybatis.mapper-locations=classpath:mapper/*.xml
# druid连接池配置
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql:///demo
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
# thymeleaf 配置
spring.thymeleaf.cache=false
1.3、编写实体类
提前建好数据库和表,按照表创建实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
private static final long serialVersionUID = -5599110107028997228L;
private Integer id;
private String name;
private String password;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Admin implements Serializable {
private static final long serialVersionUID = -947346458740614718L;
private Integer id;
private String adminname;
private String pasword;
}
1.4、编写service和mapper
User
@Service
@Transactional
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public User getUserByName(String name) {
return userMapper.getUserByName(name);
}
}
public interface UserMapper {
@Select("select * from user where name = #{name}")
User getUserByName(String name);
}
Admin
@Service
@Transactional
public class AdminServiceImpl implements AdminService {
@Autowired
private AdminMapper adminMapper;
@Override
public Admin getAdminByName(String name) {
return adminMapper.getAdminByName(name);
}
}
public interface AdminMapper {
@Select("select * from admin where adminname = #{name}")
public Admin getAdminByName(String name);
}
1.5、创建Controller
@Controller
public class UserController {
private Logger logger = LoggerFactory.getLogger(UserController.class);
@RequestMapping("userLogin")
public String userLogin(User user, Model model) {
logger.info("进入到 userLogin 控制器...");
return "user-index";
}
}
@Controller
public class AdminController {
private Logger logger = LoggerFactory.getLogger(AdminController.class);
@RequestMapping("adminLogin")
public String adminLogin(Admin admin, Model model) {
logger.info("进入到 adminLogin 控制器...");
return "admin-index";
}
}
// 做页面跳转使用
@Controller
public class AppController {
@RequestMapping("toLogin")
public String toLogin() {
return "login";
}
}
1.6、创建 realm
public class UserRealm extends AuthorizingRealm {
@Override
public String getName() {
return "UserRealm";
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
return null;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
}
public class AdminRealm extends AuthorizingRealm {
@Override
public String getName() {
return "AdminRealm";
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
return null;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
}
1.7、编写配置类
@SpringBootConfiguration
@ComponentScan("com.fu.shiro")
@MapperScan("com.fu.shiro.mapper")
public class Application {
}
shiro
@SpringBootConfiguration
public class ShiroConfig {
private Logger logger = LoggerFactory.getLogger(ShiroConfig.class);
/**
* UserRealm
*/
@Bean
public UserRealm userRealm() {
logger.info("UserRealm 执行了。。。");
return new UserRealm();
}
/**
* AdminRealm
*/
@Bean
public AdminRealm adminRealm() {
logger.info("AdminRealm 执行了。。。");
return new AdminRealm();
}
/**
* 配置安全管理器
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
Collection<Realm> realms = new ArrayList<>();
realms.add(userRealm());
realms.add(adminRealm());
defaultWebSecurityManager.setRealms(realms);
logger.info("SecurityManager 执行了。。。");
return defaultWebSecurityManager;
}
/**
* 配置shiro的过滤器
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
shiroFilterFactoryBean.setLoginUrl("/toLogin");
LinkedHashMap<String, String> map = new LinkedHashMap<>();
map.put("/toLogin", "anon");
map.put("/userLogin", "anon");
map.put("/adminLogin", "anon");
map.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
logger.info("ShiroFilterFactoryBean 执行了。。。");
return shiroFilterFactoryBean;
}
}
1.8、编写页面
login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录窗口</title>
</head>
<body>
<h3>普通用户登录</h3>
<form action="/userLogin" method="post">
用户名:<input type="text" name="name"><br>
密码:<input type="text" name="password"><br>
<input type="submit" value="用户登录">
</form>
<hr>
<h3>管理员登录</h3>
<form action="/adminLogin" method="post">
用户名:<input type="text" name="adminname"><br>
密码:<input type="text" name="password"><br>
<input type="submit" value="管理员登录">
</form>
</body>
</html>
user-index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>用户登录成功</h1>
</body>
</html>
admin-index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>管理员登录成功</h1>
</body>
</html>
2、编写核心代码
2.1、创建 LoginType
public enum LoginType {
USER("User"), ADMIN("Admin");
// 登录类型
private String loginType;
LoginType(String loginType) {
this.loginType = loginType;
}
@Override
public String toString() {
return this.loginType;
}
}
2.2、创建 CustomToken
public class CustomToken extends UsernamePasswordToken {
private static final long serialVersionUID = 9203340807922471069L;
private String loginType;
public CustomToken(String name, String password, String loginType) {
super(name, password);
this.loginType = loginType;
}
public String getLoginType() {
return loginType;
}
public void setLoginType(String loginType) {
this.loginType = loginType;
}
}
2.3、创建 CustomModularRealmAuthenticator
- 编写 自定义 realm 身份验证器(CustomModularRealmAuthenticator),继承于 ModularRealmAuthenticator ,因为在 shiro 的源码中,只有此类直接与 选择realm 有关联,详情参见 shiro源码详解—使用shiro进行登录验证 ,我们重写 doAuthenticate 方法,实现根据需求选择 realm 进行认证。
public class CustomModularRealmAuthenticator extends ModularRealmAuthenticator {
@Override
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
// 做Realm的校验,校验realm是否为空
assertRealmsConfigured();
// 获取前端传递过来的token(经过自定token修饰)
CustomToken customToken = (CustomToken) authenticationToken;
// 获取登录类型
String loginType = customToken.getLoginType();
// 根据类型判断选择哪一个realm
// 获取所有的realm
Collection<Realm> realms = getRealms();
// 将登录类型所对应的所有realm 获取到
Collection<Realm> typeRealms = new ArrayList<>();
for (Realm realm : realms) {
// 将realm类型和现在登录的类型做一个对比
if (realm.getName().contains(loginType)) {
typeRealms.add(realm);
}
}
if (typeRealms.size() == 1) {
return doSingleRealmAuthentication(typeRealms.iterator().next(), customToken);
} else {
return doMultiRealmAuthentication(typeRealms, customToken);
}
}
}
2.4、配置自定义认证器
在 ShiroConfig.java 中配置
/**
* 配置自定义认证器
*/
@Bean
public CustomModularRealmAuthenticator authenticator() {
logger.info("CustomModularRealmAuthenticator 自定义认证器执行了...");
return new CustomModularRealmAuthenticator();
}
/**
* 配置安全管理器
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
// 设置身份验证器,一定要设置在realm之前,否则获取不到realm,getRealms() 为空
defaultWebSecurityManager.setAuthenticator(authenticator());
Collection<Realm> realms = new ArrayList<>();
realms.add(userRealm());
realms.add(adminRealm());
defaultWebSecurityManager.setRealms(realms);
logger.info("SecurityManager 执行了。。。");
return defaultWebSecurityManager;
}
2.5、修改 controller
UserController.java
@Controller
public class UserController {
//用户登陆的类型
private static final String LOGIN_TYPE= LoginType.USER.toString();
private Logger logger = LoggerFactory.getLogger(UserController.class);
@RequestMapping("userLogin")
public String userLogin(User user) {
logger.info("进入到 userLogin 控制器...");
// 封装请求对象为token
CustomToken customToken = new CustomToken(user.getName(), user.getPassword(), LOGIN_TYPE);
// 获取登录主体
Subject subject = SecurityUtils.getSubject();
// 登录
try {
subject.login(customToken);
if (subject.isAuthenticated()) {
// 如果认证成功
return "user-index";
}
} catch (UnknownAccountException e) {
logger.info("用户名不正确");
} catch (IncorrectCredentialsException e) {
logger.info("密码不正确");
} catch (Exception e) {
logger.info("其他错误造成了登陆错误");
}
return "login";
}
}
AdminController.java
@Controller
public class AdminController {
private Logger logger = LoggerFactory.getLogger(AdminController.class);
private static final String LOGIN_TYPE = LoginType.ADMIN.toString();
@RequestMapping("adminLogin")
public String adminLogin(Admin admin) {
logger.info("进入到 adminLogin 控制器...");
// 封装前端传递的数据到 token
CustomToken customToken = new CustomToken(admin.getAdminname(), admin.getPassword(), LOGIN_TYPE);
// 获取登录主体
Subject subject = SecurityUtils.getSubject();
// 登录
try {
subject.login(customToken);
if (subject.isAuthenticated()) {
return "admin-index";
}
} catch (UnknownAccountException e) {
logger.info("用户名不正确");
} catch (IncorrectCredentialsException e) {
logger.info("密码不正确");
} catch (Exception e) {
logger.info("其他错误造成了登陆错误");
}
return "login";
}
}
2.6、修改 realm
UserRealm.java
public class UserRealm extends AuthorizingRealm {
private Logger logger = LoggerFactory.getLogger(UserRealm.class);
@Autowired
private UserService userService;
@Override
public String getName() {
return "UserRealm";
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
logger.info("UserRealm 执行了...");
String userName = (String) authenticationToken.getPrincipal();
User user = userService.getUserByName(userName);
if (null == user) {
return null;
}
return new SimpleAuthenticationInfo(user.getName(), user.getPassword(), getName());
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
}
AdminRealm.java
public class AdminRealm extends AuthorizingRealm {
private Logger logger = LoggerFactory.getLogger(AdminRealm.class);
@Autowired
private AdminService adminService;
@Override
public String getName() {
return "AdminRealm";
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
logger.info("AdminRealm 执行了...");
// 获取用户名
String adminname = (String) authenticationToken.getPrincipal();
Admin admin = adminService.getAdminByName(adminname);
if (null == admin) {
return null;
}
return new SimpleAuthenticationInfo(admin.getAdminname(), admin.getPassword(), getName());
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
}
3、总结
- 注意编写完自定义认证器后,需要在 ShiroConfig 中进行配置。
- 在安全管理中设置 身份验证器 时,一定要设置在realm之前,否则获取不到realm,getRealms() 为空。
- 多 realm 的控制,主要是通过对 ModularRealmAuthenticator 类中 doAuthenticate方法 的重写,按照我们自己需要的逻辑来进行配置。