二、Shiro安全框架-登陆实现
写在前面
上一章我们已经了解了Shiro的基本信息和架构,接下来我们用Shiro实现一个简单的登录。
1.登陆实现
1.1.搭建项目
创建一个名为”shiro-demo“的基于SpringBoot的web工程。
1.2.导入依赖
继承SpringBoot的父工程,导入web依赖和shiro整合spring的依赖以及其他相关依赖。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.5.RELEASE</version>
</parent>
<dependencies>
<!-- 单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
<version>4.12</version>
</dependency>
<!-- web依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- spring测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!-- lombok依赖 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- shiro整合spring依赖 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<!-- spring-boot整合mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
<!-- mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
<!-- 阿里巴巴数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.6</version>
</dependency>
</dependencies>
1.3.主启动类
@SpringBootApplication
@MapperScan("cn.demo.shiro.mapper")
public class StartApplication {
public static void main(String[] args) {
SpringApplication.run(StartApplication.class);
}
}
1.4.application.yml配置
配置端口和数据库相关
server:
port: 8000
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql:///shiro
driver-class-name: com.mysql.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
mybatis:
mapper-locations: classpath:cn/demo/shiro/mapper/*Mapper.xml
1.5.web控制器
@RestController
@RequestMapping("/user")
public class UserController {
/**
* ----------------------------------------------------------
* 登录方法
* ----------------------------------------------------------
**/
@RequestMapping("/login")
public String login(String username, String password){
Subject subject = SecurityUtils.getSubject();
try {
if (!subject.isAuthenticated()){
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// 调用shiro的login方法
subject.login(token);
}
return "用户名:"+username+"登陆成功!";
} catch (UnknownAccountException uae ) {
// 未知用户的帐号
return "用户名错误!";
} catch (IncorrectCredentialsException ice ) {
// 密码不匹配
return "密码错误!";
} catch (LockedAccountException lae ) {
// 该帐号被锁定了,无法登录。
return "账号已被锁定!";
} catch (AuthenticationException ae ) {
return "身份认证失败!";
}
}
/**
* ----------------------------------------------------------
* 获取用户列表方法,在shiro过滤配置中配置这个路径可以匿名访问
* ----------------------------------------------------------
**/
@RequestMapping("/list")
public String getList(){
return "/user/list";
}
/**
* ----------------------------------------------------------
* 添加用户的方法,在shiro过滤配置中配置这个路径需要认证后可访问
* ----------------------------------------------------------
**/
@RequestMapping("/add")
public String addUser(){
return "/user/add";
}
/**
* ----------------------------------------------------------
* 未登录,在前后端分离项目中返回json字符串给前端即可,单体项目可以
* 重定向到登陆界面
* ----------------------------------------------------------
**/
@RequestMapping("/unlogin")
public String unlogin(){
return "{'code':9999;msg:'未登录!'}";
}
}
1.6.自定义Realm
自定义的Realm我们一般通过继承AuthorizingRealm类来做自己具体的实现。
public class MyRealm extends AuthorizingRealm {
@Resource
private UserMapper userMapper;
/**
* 返回当前领域的名称,必须唯一
*/
@Override
public String getName() {
return "myRealm";
}
/**
* 判断领域是否支持传入的token
*/
@Override
public boolean supports(AuthenticationToken token) {
// 表示当前realm只支持UsernamePasswordToken类型的token
return token instanceof UsernamePasswordToken;
}
/**
* 返回根据token拿到的认证信息
* 一般我们只需要拿到账户信息即可
*/
@Override
public AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String)token.getPrincipal(); //得到用户名
User user = userMapper.selectByUsername(username);
if(Objects.isNull(user)) {
throw new UnknownAccountException("用户名错误,请重新输入用户名!"); //如果用户名错误
}
//如果身份认证验证成功,返回一个AuthenticationInfo实现;
return new SimpleAuthenticationInfo(user, user.getPassword(), getName());
}
/**
* 返回用户的授权信息
* 授权相关后面再说,这篇文章只关注于认证
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
}
1.7.Shiro配置
@Configuration
public class ShiroConfig {
/**
* ----------------------------------------------------------
* 配置Reaml
* ----------------------------------------------------------
**/
@Bean
public MyRealm getMyRealm(){
MyRealm myRealm = new MyRealm();
myRealm.setCredentialsMatcher(new HashedCredentialsMatcher(Sha256Hash.ALGORITHM_NAME));
return myRealm;
}
/**
* ----------------------------------------------------------
* 配置安全管理器
* ----------------------------------------------------------
**/
@Bean
public SecurityManager securityManager(Realm realm){
return new DefaultWebSecurityManager(realm);
}
/**
* ----------------------------------------------------------
* 配置shiro过滤器
*
* 过滤类型:
* anon:无需认证就可以访问
* authc:必须认证了才能访问
* user:必须用有了"记住我"功能才能用
* perms:拥有对某个资源的权限才能访问
* role:拥有某个角色权限才能访问
* ----------------------------------------------------------
**/
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
HashMap<String, String> filterChainDefinitionMap = new HashMap<>();
filterChainDefinitionMap.put("/user/login", "anon");
filterChainDefinitionMap.put("/user/list", "anon");
filterChainDefinitionMap.put("/**", "authc");
// 设置安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 配置shiro默认登录界面地址,前后端分离中登录界面跳转应由前端路由控制,后台仅返回json数据
// 如果不进行配置那么默认为类路径下的login.jsp
shiroFilterFactoryBean.setLoginUrl("/user/unlogin");
// 设置登录成功后跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/user/index");
// 设置未授权跳转的url
shiroFilterFactoryBean.setUnauthorizedUrl("/user/unAuthorized");
// 设置资源过滤集合
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* ----------------------------------------------------------
* 配置加密方式,使用Shiro自带的Hash256进行加密
* ----------------------------------------------------------
**/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName(Sha256Hash.ALGORITHM_NAME);
return hashedCredentialsMatcher;
}
}
2.模拟登录,访问资源
打开浏览器,在地址栏输入localhost:8000/user/list
在未登录的情况下成功访问到资源,因为我们在shiro的过滤配置中对这个资源设置的无需认证就可以访问
在地址栏输入localhost:8000/user/add,点击回车后显示未登录信息
因为我们在shiro的过滤配置中没有对/user/add这个资源进行单独配置访问权限,那么这个资源会被所配置的通配符配置进行拦截到,所以该资源就需要认证之后才能进行访问
在地址栏输入localhost:8000/user/login?username=root&password=root,点击回车后进行登录认证
这个时候我们可以看到已经显示登陆成功,这个时候再访问/user/add则可以成功访问
到这里我们的简单认证登录实现就已经完成了
3.认证流程小结
SpringBoot整合Shrio的登录认证案例到这里就结束了,这篇文章没有对原理做过多的介绍,只是带大家进行一个shiro认证实现过程,这个案例的认证流程如下:
-
用户在输入用户名密码提交登陆后会由Shiro来为我们进行认证,我们将用户输入的用户名和密码封装成Shiro认证所需要的UsernamePasswordToken对象,然后调用Sebject的login方法
-
Shrio使用DefualtSecurityManager调用ModularRealmAuthenticator领域认证器选择数据源
-
我们自定义的Realm为登录认证提供安全数据(用户名、密码),通过用户名到数据库进行匹配
-
使用凭证匹配器(通过我们配置的密码编码器)来进行密码匹配,如果匹配成功就跳转到登录页面,认证失败就返回错误信息
4.总结
本篇文章到这里就结束了,如果觉得对屏幕前的你有帮助的话就请来个一键三连吧,欢迎大家学习讨论,如果有不足之处请尽情指出,大家一起进步,学无止尽,Never Give Up!!!