文章目录
1.概述
1.1 SpringBoot
今天要做的是使用SpringBoot配合Shiro来实现登陆的认证,所以SpringBoot是必不可少的,相信大家能用到Shiro了,SpringBoot一定不差,那就不做过多赘述,我们主要来介绍Shiro。
1.2 Shiro
Shiro是java的一个安全框架,它功能强大而且易于使用,相对于 Spring Security受用性更广泛,而且Shiro在保持强大功能的同时,还在简单性和灵活性方面拥有巨大优势。
Shiro的功能也包括以下几点:
- Authentication:身份认证,验证用户是否拥有某个身份。
- Authorization: 权限校验,验证某个已认证的用户是否拥有某个权限。确定“谁”可以访问“什么”。
- Session Management:会话管理,管理用户登录后的会话,
- Cryptography:加密,使用密码学加密数据,如加密密码。
- Web Support:Web支持,能够比较轻易地整合到Web环境中。
- Caching:缓存,对用户的数据进行缓存,
- Concurrency:并发,Apache Shiro支持具有并发功能的多线程应用程序,也就是说支持在多线程应用中并发验证。
- Testing:测试,提供了测试的支持。
- Run as :允许用户以其他用户的身份来登录。
- Remember me :记住我
其中Authentication身份认证配合Authorization权限校验一起食用效果更佳哦!
我们今天主要来讲如何实现身份的认证
2.Shiro实现登录认证
在使用Shiro进行登录认证之前,我们得先了解清楚几个概念性的问题,首先Shiro作为一个优秀的安全框架,那它一定有着一个中枢管理中心,没错就是DefaultWebSecurityManager这个类,之后我们所自定义的方法对象都会添加到这个管理中心去,会由它来调控我们所定义的方法从而代替系统自带的默认认证方法。其次呢Realm也是一个很重要的对象,它翻译过来叫做"域",是由它充当了 Shiro 与应用安全数据间的“桥梁”或者“连接器”。我们所自定义的方法需要写在继承了AuthorizingRealm的类中,才能添加到管理中心去。最后一个概念是Subject,通常我们会将Subject对象理解为一个用户,同样的它也有可能是一个三方程序,它是一个抽象的概念,可以理解为任何与系统交互的“东西”都是Subject。等会我们也就是通过它进行登录的认证。
导入pom依赖
先创建一个SpringBoot的项目并导入shiro和springboot的pom依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--shiro-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
配置好后我们去设置配置文件application.yml,去里面设置我们的springboot启动端口号。
server:
port: 8080 #自定义
然后设置我们的SpringBoot启动类。
@SpringBootApplication
public class ShiroApplication {
public static void main(String[] args) {
SpringApplication.run(ShiroApplication.class);
}
}
配置核心方法
之后我们配置一个Controller方法,用来与页面交接,传入账号密码进行测试登录情况以及测试登录前后的权限情况。
package com.df.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
@RestController
public class LoginController {
//如果需要使用shiro长期登陆,设置subject的rememberMe属性并且设置允许的范围为user。authc不允许被rememberMe用户访问。
//这就是我们传入账号密码测试的地方
@PostMapping(value = "/doLogin")
public void doLogin(@RequestParam(value = "username") String username,
@RequestParam(value = "password") String password){
Subject subject = SecurityUtils.getSubject();
try {
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, password);
subject.login(usernamePasswordToken);
System.out.println("登陆成功");
}catch (Exception e){
e.printStackTrace();
System.out.println("登陆失败");
}
}
@RequestMapping(value = "/index")
public String index(){
System.out.println("欢迎来到主页");
return "欢迎来到主页";
}
//我们可以使用postman进行调用测试 登录前后hello的区别
@GetMapping(value = "/hello")
public String hello(HttpServletRequest request){
Cookie[] cookies = request.getCookies();
System.out.println(cookies[0].getValue());
return "hello";
}
//用来设置未登录用户跳转的方法
@GetMapping(value = "/login")
public String login(){
return "Please Login !";
}
//注销方法
@GetMapping(value = "/logout")
public String logout(){
Subject subject = SecurityUtils.getSubject();
subject.logout();
System.out.println("成功退出");
return "success to logout";
}
}
设置好Controller的方法后,我们去创建一个Realm,用来设置登录账号认证。创建一个config文件然后设置一个MyRealm类继承AuthorizingRealm。
然后来实现认证的过程,继承好后需要实现两个方法,其中一个是认证的方法,一个是授权的方法,我们这节只需要完成认证的方法就行了。
package com.df.config;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
/**
* @author Lin
* @create 2020/7/15
* @since 1.0.0
* (功能):
*/
public class MyRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override //认证
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//从被shiro封装成的token中取出我们传入的username
String username = (String) authenticationToken.getPrincipal();
//这里应有一步去缓存或数据库查询的步骤,我省略了
//我直接定义了一个username,如果用户名不匹配,则报错用户名不存在。
if(!"LinJy".equals(username)){
throw new UnknownAccountException("账号不存在");
}
//返回一个新封装的认证实体,传入的是用户名,数据库查出来的密码,和当前Realm的名字
return new SimpleAuthenticationInfo(username, "123", this.getName());
}
}
在以上我们就将账号认证的过程完成了,肯定有很多小伙伴们问,账号认证完了,密码呢?密码为什么不一起认证掉。咱接下来就开始写认证密码的步骤。shiro将账号和密码分成两个地方进行验证,如果我们不自己定义,那么就会调用shiro默认的校验方法。
我们重新创建一个MyCredentialsMatcher类继承SimpleCredentialsMatcher来实现我们自定义的密码校验方法。
package com.df.config;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
public class MyCredentialsMatcher extends SimpleCredentialsMatcher {
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
UsernamePasswordToken tokenResolve = (UsernamePasswordToken) token;
String tokenPwd = new String(tokenResolve.getPassword());
String infoPwd =(String) info.getCredentials();
//调用当前类重写的equals方法来对比两个password是否一致,返回对比结果
return super.equals(tokenPwd, infoPwd);
}
}
我们设置完账号了密码之后需要设置一个管理器将我们验证账号和密码的组件拉近shiro中去。没错又到了我们的配置类出场了。配置类的主要作用就是将我们之前所做的一些配件组装到一起,并设置一个拦截器对我们需要登录的请求进行登录拦截。不多说直接上代码,具体注释代码里有。
package com.df.config;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
//引入之前定义好的域
@Bean
MyRealm myRealm(){
return new MyRealm();
}
//配置一个安全管理器
@Bean
DefaultWebSecurityManager securityManager(){
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
MyRealm myRealm = myRealm();
//将我们配置好的密码校验放入域中
myRealm.setCredentialsMatcher(myCredentialsMatcher());
//将域添加到我们的安全管理器中
manager.setRealm(myRealm);
//设置Session管理器,配置shiro中Session的持续时间
manager.setSessionManager(getDefaultWebSessionManager());
return manager;
}
//引入密码校验
@Bean
public MyCredentialsMatcher myCredentialsMatcher(){
return new MyCredentialsMatcher();
}
//设置session过期时间
@Bean
public DefaultWebSessionManager getDefaultWebSessionManager() {
DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
defaultWebSessionManager.setGlobalSessionTimeout(1000 * 60);// 会话过期时间,单位:毫秒--->一分钟,用于测试
defaultWebSessionManager.setSessionValidationSchedulerEnabled(true);
defaultWebSessionManager.setSessionIdCookieEnabled(true);
return defaultWebSessionManager;
}
//设置访问拦截器
@Bean
ShiroFilterFactoryBean shiroFilterFactoryBean(){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//传入安全管理器
bean.setSecurityManager(securityManager());
//传入未登录用户访问登陆用户的权限所跳转的页面
bean.setLoginUrl("/login");
//设置成功后返回页面
bean.setSuccessUrl("/index");
//访问未授权网页所跳转的页面
bean.setUnauthorizedUrl("/unauthorized");
Map<String, String> map = new LinkedHashMap<>();
//允许 需要设置login为anon 否则登陆成功后无法成功跳转。
map.put("/login", "anon");
map.put("/doLogin", "anon");
map.put("/index", "anon");
//设置所有的请求未登录不允许进入。
map.put("/**", "authc");
bean.setFilterChainDefinitionMap(map);
return bean;
}
}
然后我们就可以启动SpringBoot一试,建议先访问被拦截的请求如:/hello,会发现无法访问,再进行登录后:/doLogin,再进行访问hello。源码我会传到github,地址链接在最下面,附送一份dubbo+zookeeper整合shiro的。
3.Shiro登录认证源码解析
接下来让我们深入探究,shiro是如何一步步实现进行对账号和密码的校验的。首先还是从我们熟悉的/doLogin方法开始进入。
进入后我们发现shiro将我们输入的token交给了安全管理器进行调用
进入安全管理器后,发现安全管理器调用了一个login方法,传入了我们的token,通过进入这个方法来进行Realm的账号认证。
上述进行判断我们传入的token是否有问题,如果没有则进入正题,调用进行认证的方法,从下面的报错可以了解到,里面会对我们的需要认证账号进行一个判断。
然后来到这里,其中第一步的this.assertRealmConfigured()是对Realm的一个判断,里面进行判断Realm是否是一个空值,然后获取Realm。因为我们之前配置了一个Realm,所以会调用唯一的那个Realm,不然的话会进行遍历Realm,然后一个个进行调用判断。以下是调用多个Realm的代码。
我们再深入,可以看到它先对realm是否支持该token进行了一个判断,支持的话则调用realm进行认证。
然后进入realm先对以前登录过的缓存进行获取,如果为空的话才开始认证的过程,
哎哟!可算回到我们熟悉的方法来了,这就是我们之前定义的对用户名进行判断的方法。判断成功后将用户名和数据库查询到的密码重新封装。然后返回。
执行完对账号的认证后返回了info信息,可以在图片上看见,如果info不为null,则进行密码的验证。
进入后先获取了一个CredentialsMatcher对象,该对象我们找到它的接口实现类,发现我们已经实现了那个密码匹配的方法,所以会调用我们自定义的对象。当然如果我们没有自定义,那么就会调用默认的加密密码对比认证。
然后我们继续往下,进入到密码认证的阶段。
又进入我们熟悉的地方了,进行两个密码的验证。调用该类的方法进行密码的验证,实际上是进行单个字符遍历比较,最终不正确的话返回false;
然后最终就结束啦。完结撒花~下面放一下该项目的地址,需要的小伙伴自取。