shrio是一个很好的登陆以及权限管理框架,但是mo默认是单realm单数据表,如果业务中用户分布在不同的数据表,单realm就很难实现登陆以及权限管理的功能,这篇博客就简单的介绍一个家长 学生 老师的shiro的多realm登陆验证,使用springboot,mybatis mysql等相关技术,博客底部附上源码,有兴趣的可以去下载
1.项目pom依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<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>1.3.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
2.用户数据表
CREATE DATABASE /*!32312 IF NOT EXISTS*/`springboot_shiro_mulit_relam` /*!40100 DEFAULT CHARACTER SET utf8 */;
USE `springboot_shiro_mulit_relam`;
/*Table structure for table `parent` */
DROP TABLE IF EXISTS `parent`;
CREATE TABLE `parent` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
`account` varchar(20) DEFAULT NULL,
`password` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
/*Data for the table `parent` */
insert into `parent`(`id`,`name`,`account`,`password`) values (1,'张家长','123','123');
/*Table structure for table `student` */
DROP TABLE IF EXISTS `student`;
CREATE TABLE `student` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
`account` varchar(20) DEFAULT NULL,
`password` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
/*Data for the table `student` */
insert into `student`(`id`,`name`,`account`,`password`) values (1,'王同学','456','456');
/*Table structure for table `teacher` */
DROP TABLE IF EXISTS `teacher`;
CREATE TABLE `teacher` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
`account` varchar(20) DEFAULT NULL,
`password` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
/*Data for the table `teacher` */
insert into `teacher`(`id`,`name`,`account`,`password`) values (1,'刘老师','789','789');
3.shiro 配置
实现多realm的关键在于继承shiro默认的UsernamePasswordToken,添加一个字段用于标识不同的realm,用户登录的shi时候带上标识的字段,shiro则根据字段去不同的realm去验证登陆,授权等
3.1 继承UsernamePasswordToken添加loginType属性
public class UserToken extends UsernamePasswordToken {
private String loginType;
public UserToken() {}
public UserToken(final String username, final String password,
final String loginType) {
super(username, password);
this.loginType = loginType;
}
public String getLoginType() {
return loginType;
}
public void setLoginType(String loginType) {
this.loginType = loginType;
}
}
3.2 登陆时使用继承后的UserToken,添加loginType属性
@RequestMapping("/studentLogin")
public Student studentLogin(@RequestParam(value = "account") String account,
@RequestParam(value = "password") String password,
HttpServletRequest request,
HttpServletResponse response) {
System.out.println("\n学生登陆");
Student student = null;
//设置永不过期
SecurityUtils.getSubject().getSession().setTimeout(-1000L);
Subject subject = SecurityUtils.getSubject();
try {
// 调用安全认证框架的登录方法
subject.login(new UserToken(account, password, "Student"));
student = studentService.getStudentByAccount(account);
} catch (AuthenticationException ex) {
System.out.println("登陆失败: " + ex.getMessage());
}
return student;
}
3.3 获得AuthenticationToken,判断是单realm还是多realm,分别去不同的方法验证
public class UserModularRealmAuthenticator extends ModularRealmAuthenticator {
@Override
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken)
throws AuthenticationException {
System.out.println("UserModularRealmAuthenticator:method doAuthenticate() execute ");
// 判断getRealms()是否返回为空
assertRealmsConfigured();
// 强制转换回自定义的CustomizedToken
UserToken userToken = (UserToken) authenticationToken;
// 登录类型
String loginType = userToken.getLoginType();
// 所有Realm
Collection<Realm> realms = getRealms();
// 登录类型对应的所有Realm
List<Realm> typeRealms = new ArrayList<>();
for (Realm realm : realms) {
if (realm.getName().contains(loginType)) {
typeRealms.add(realm);
}
}
// 判断是单Realm还是多Realm
if (typeRealms.size() == 1){
System.out.println("doSingleRealmAuthentication() execute ");
return doSingleRealmAuthentication(typeRealms.get(0), userToken);
}
else{
System.out.println("doMultiRealmAuthentication() execute ");
return doMultiRealmAuthentication(typeRealms, userToken);
}
}
}
3.4 配置明文密码登陆,方便测试
public class CustomCredentialsMatcher extends SimpleCredentialsMatcher {
@Override
public boolean doCredentialsMatch(AuthenticationToken authcToken, AuthenticationInfo info) {
UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
// Object tokenCredentials = encrypt(String.valueOf(token.getPassword()));
//明文密码
Object tokenCredentials = String.valueOf(token.getPassword());
Object accountCredentials = getCredentials(info);
//将密码加密与系统加密后的密码校验,内容一致就返回true,不一致就返回false
return equals(tokenCredentials, accountCredentials);
}
/**
* @author Adam
* @description 将传进来密码加密方法
* @date 11:26 2018/9/2
* @param [data]
* @return java.lang.String
*/
private String encrypt(String data) {
//这里可以选择自己的密码验证方式 比如 md5或者sha256等
String sha384Hex = new Sha384Hash(data).toBase64();
return sha384Hex;
}
}
3.5 shiroconfig 配置 (将三个用户realm 注入)
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
System.out.println("ShiroConfiguration.shirFilter()");
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//拦截器.
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();
// 配置不会被拦截的链接 顺序判断
filterChainDefinitionMap.put("/image/**", "anon");
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/html/**", "anon");
//测试开放的接口
filterChainDefinitionMap.put("/login/**", "anon");
filterChainDefinitionMap.put("/favicon.ico", "anon");
//配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了
filterChainDefinitionMap.put("/logout", "logout");
//<!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;
//<!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
// filterChainDefinitionMap.put("/**", "authc");
filterChainDefinitionMap.put("/**", "anon");
// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl("/login/unauth");
//未授权界面;
shiroFilterFactoryBean.setUnauthorizedUrl("/login/unauth");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* 凭证匹配器
* (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了)
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashAlgorithmName("md5");
//散列的次数,比如散列两次,相当于 md5(md5(""));
hashedCredentialsMatcher.setHashIterations(2);
return hashedCredentialsMatcher;
}
@Bean(name = "parentRealm")
public ParentRealm parentRealm(){
ParentRealm parentRealm = new ParentRealm();
parentRealm.setCredentialsMatcher(new CustomCredentialsMatcher());
return parentRealm;
}
@Bean(name = "teacherRealm")
public TeacherRealm teacherRealm(){
TeacherRealm teacherRealm = new TeacherRealm();
teacherRealm.setCredentialsMatcher(new CustomCredentialsMatcher());
return teacherRealm;
}
@Bean(name = "studentRealm")
public StudentRealm studentRealm(){
StudentRealm studentRealm = new StudentRealm();
studentRealm.setCredentialsMatcher(new CustomCredentialsMatcher());
return studentRealm;
}
@Bean(name = "SecurityManager")
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//设置realm.
securityManager.setAuthenticator(modularRealmAuthenticator());
List<Realm> realms = new ArrayList<>();
//添加多个Realm
realms.add(parentRealm());
realms.add(teacherRealm());
realms.add(studentRealm());
securityManager.setRealms(realms);
return securityManager;
}
/**
* 开启shiro aop注解支持.
* 使用代理方式;所以需要开启代码支持;
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean(name="simpleMappingExceptionResolver")
public SimpleMappingExceptionResolver
createSimpleMappingExceptionResolver() {
SimpleMappingExceptionResolver r = new SimpleMappingExceptionResolver();
Properties mappings = new Properties();
//数据库异常处理
mappings.setProperty("DatabaseException", "databaseError");
mappings.setProperty("UnauthorizedException","/403");
r.setExceptionMappings(mappings);
r.setDefaultErrorView("error");
r.setExceptionAttribute("ex");
return r;
}
/**
* 系统自带的Realm管理,主要针对多realm
* */
@Bean
public ModularRealmAuthenticator modularRealmAuthenticator(){
//自己重写的ModularRealmAuthenticator
UserModularRealmAuthenticator modularRealmAuthenticator = new UserModularRealmAuthenticator();
modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
return modularRealmAuthenticator;
}
3.6 realm 注入service层,去数据库中查找数据,验证密码
public class ParentRealm extends AuthorizingRealm {
@Autowired
private ParentService parentService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("权限配置-->MyShiroRealm.doGetAuthorizationInfo()");
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
return authorizationInfo;
}
/**
* @Author Adam
* @Description 主要是用来进行身份认证的,也就是说验证用户输入的账号和密码是否正确
* @Date 14:06 2018/9/28
* @Param [token]
* @return org.apache.shiro.authc.AuthenticationInfo
**/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
System.out.println("MyShiroRealm.doGetAuthenticationInfo()");
//获取用户的输入的账号.
String account = (String)token.getPrincipal();
Object credentials = token.getCredentials();
System.out.println("credentials="+credentials);
//通过username从数据库中查找对象
//实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
Parent parent = parentService.getParentByAccount(account);
System.out.println("----->>parent="+parent);
if(parent == null){
return null;
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
parent,
parent.getPassword(),
getName()
);
return authenticationInfo;
}
}
public class StudentRealm extends AuthorizingRealm {
@Autowired
private StudentService studentService;
/**
* @Author Adam
* @Description 权限配置
* @Date 14:29 2018/9/28
* @Param [principals]
* @return org.apache.shiro.authz.AuthorizationInfo
**/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("权限配置-->MyShiroRealm.doGetAuthorizationInfo()");
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
return authorizationInfo;
}
/**
* @Author Adam
* @Description 主要是用来进行身份认证的,也就是说验证用户输入的账号和密码是否正确。
* @Date 14:24 2018/9/28
* @Param [token]
* @return org.apache.shiro.authc.AuthenticationInfo
**/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
System.out.println("MyShiroRealm.doGetAuthenticationInfo()");
//获取用户的输入的账号.
String account = (String)token.getPrincipal();
Object credentials = token.getCredentials();
System.out.println("credentials="+credentials);
//通过username从数据库中查找对象
Student student = studentService.getStudentByAccount(account);
System.out.println("----->>student="+student);
if(student == null){
return null;
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
student,
student.getPassword(),
getName()
);
return authenticationInfo;
}
}
public class TeacherRealm extends AuthorizingRealm {
@Autowired
private TeacherService teacherService;
/**
* @Author Adam
* @Description 权限配置
* @Date 14:09 2018/9/28
* @Param [principals]
* @return org.apache.shiro.authz.AuthorizationInfo
**/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("权限配置-->MyShiroRealm.doGetAuthorizationInfo()");
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
return authorizationInfo;
}
/**
* @Author Adam
* @Description 主要是用来进行身份认证的,也就是说验证用户输入的账号和密码是否正确
* @Date 14:08 2018/9/28
* @Param [token]
* @return org.apache.shiro.authc.AuthenticationInfo
**/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
System.out.println("MyShiroRealm.doGetAuthenticationInfo()");
//获取用户的输入的账号.
String account = (String)token.getPrincipal();
Object credentials = token.getCredentials();
System.out.println("credentials="+credentials);
//通过username从数据库中查找对象
//实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
Teacher teacher = teacherService.getTeacherByAccount(account);
System.out.println("----->>teacher="+teacher);
if(teacher == null){
return null;
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
teacher,
teacher.getPassword(),
getName()
);
return authenticationInfo;
}
}