一、什么是Shiro?
Apache Shiro 是一个Java 的安全(权限)框架。Shiro可以完成,认证,授权,加密,会话管理,Web集成,缓存等。
二、Shiro的主要功能
Authentication:身份认证、登录,验证用户是不是拥有相应的身份。
Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限,即判断用户能否进行什么操作。
Session Manager:会话管理,即用户登录后就是第一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通的JavaSE环境,也可以是Web环境;
Cryptography:加密,保护数据的安全性,如密码加密存储到数据库中,而不是明文存储;
Caching:缓存,比如用户登录后,其用户信息,拥有的角色、权限不必每次去查,这样可以提高效率
Concurrency:Shiro支持多线程应用的并发验证,即,如在一个线程中开启另一个线程,能把权限自动的传播过去
Testing:提供测试支持;
Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了
三、Shiro的整体架构
从外部来看Shiro,即从应用程序角度来观察如何使用shiro完成工作:
Shiro的主要架构就是上面圈起来的三个模块,Subject、SecurityManager、Realm
Subject:应用代码直接交互的对象是Subject,也就是说Shiro的对外API核心就是Subject,Subject代表了当前的用户,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,也可以是第三方进程、后台帐户、网络爬虫、机器人等。与Subject的所有交互都会委托给SecurityManager;Subject其实是一个门面,SecurityManageer 才是实际的执行者
SecurityManager:安全管理器,即所有与安全有关的操作都会与SercurityManager交互,并且它管理着所有的Subject,且负责进行认证,授权,会话,及缓存的管理。可以看出它是Shiro的核心,它负责与Shiro的其他组件进行交互,它相当于SpringMVC的DispatcherServlet的角色,是Shiro的心脏。
Realm:Shiro从Realm获取安全数据(如用户,角色,权限),也就是说SecurityManager要验证用户的数据和权限是否正确,那需要先从Realm中先读取到该用户的数据,然后才能验证。简单点来说,其实Realm就是用来读取用户数据的,可以把它当成数据库,从realm中读取用户信息,也就是从数据库中查用户信息,用户的权限,角色等。
Shiro的特点就是层次分明的很清晰,所以学习和用起来就很方便易于上手。
四、SpringBoot中集成Shiro
- 导入依赖,主要就是shiro_spring整合的那个包
<!-- 导入shiro-spring整合包 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.7.1</version>
</dependency>
<!--thymeleaf-shiro整合包-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<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>
- 编写配置,首先需要创建一个Realm对象,因为Shiro需要通过Realm来获取用户信息,继承AuthorizingRealm并重写两个方法,授权和认证,安全框架中最主要的也就是授权和认证
public class UserRealm extends AuthorizingRealm{
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了---》授权逻辑:PrincipalCollection");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
Subject subject = SecurityUtils.getSubject();// 获取subject对象
User currentUser = (User)subject.getPrincipal();// 获取认证时传过来的user对象
info.addStringPermissions(currentUser.getPerms());// 设置权限
// 用户登录后,把用户信息放到session
subject.getSession().setAttribute("loginUser",currentUser);
return info;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("执行了---》认证逻辑:AuthenticationToken");
// 1. 判断用户名
UsernamePasswordToken userToken= (UsernamePasswordToken) token;
User user = new User();
user.setUsername(userToken.getUsername());
// 这里模拟用数据库,根据用户传来的用户名去数据库查询对应的user信息
user=queryUserByUserName(user);
if(user==null){// 用户名不存在
return null;// 返回null, shiro底层就会输出UnknownAccountException,然后被我们的controller捕获到这个异常,就知道就是用户名不存在了
}
// 2. 验证密码 返回一个AuthenticationInfo的实现类
// shiro会自动帮我们验证!
// 第一个参数,放入user,那么上面的授权代码中就可以获取到该user信息,也就获取到该user对应的权限了
// 重点是第二个参数就是从该用户名对应的 数据库中的真实的密码 !
return new SimpleAuthenticationInfo(user,user.getPassword(),"");
}
// 为了省事,这里模拟从数据库查询的用户信息的方法
public User queryUserByUserName(User user){
Set set=new HashSet();
if(user.getUsername().equals("root")){
user.setPassword("123456");
set.add("user:add");
set.add("user:update");
user.setPerms(set);
}else if(user.getUsername().equals("admin")){
user.setPassword("123456");
set.add("user:add");
user.setPerms(set);
}else{
user=null;
}
return user;
}
}
- 编写Shiro的配置类
// 这里主要就是配置shiro的三个对象bean,三个bean都是互相关联,层层嵌套的
@Configuration
public class ShiroConfig {
//创建 ShiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 设置安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);
/* 添加Shiro内置过滤器,常用的有如下过滤器:
anon: 无需认证就可以访问
authc: 必须认证才可以访问
user: 如果使用了记住我功能就可以直接访问
perms: 拥有某个资源权限才可以访问
role: 拥有某个角色权限才可以访问
*/
Map<String, String> filterMap = new LinkedHashMap<String, String>();
// 设置页面对应权限,有该权限才可访问,注意各种权限的配置的顺序
filterMap.put("/user/add", "perms[user:add]");
filterMap.put("/user/update", "perms[user:update]");
//filterMap.put("/user/*","authc");//authc 登录就可以访问,可使用通配符
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
//设置登录页面
shiroFilterFactoryBean.setLoginUrl("/toLogin");
// 设置未授权展示页面
shiroFilterFactoryBean.setUnauthorizedUrl("/noAuth");
return shiroFilterFactoryBean;
}
// 创建 DefaultWebSecurityManager
@Bean(name = "securityManager")// 指定bean的名称,不指定默认就是方法名
public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置Realm对象
securityManager.setRealm(userRealm);
return securityManager;
}
// 创建 realm 对象
@Bean
public UserRealm userRealm() {
return new UserRealm();
}
//配置ShiroDialect:方言,用于 thymeleaf 和 shiro 标签配合使用
@Bean
public ShiroDialect getShiroDialect() {
return new ShiroDialect();
}
}
- 控制层Controller , 主要看login方法
// 登录页面
@RequestMapping({"/toLogin"})
public String toLogin(){
return "login";
}
// 点击登录,验证逻辑
@RequestMapping({"/login"})
public String login(String username, String password, Model model){
// shiro的认证逻辑,开始了! (也就是用户名和密码的校验逻辑)
//1. 获取subject
Subject subject = SecurityUtils.getSubject();
//2. 封装用户的数据(就是把页面传过来的用户名和密码进行封装,封装为一个token,用这个token去走验证逻辑)
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
//3. 执行登录方法
try {
subject.login(token);// 这个login方法很重要,它会去走UserRealm中的认证逻辑
return "index"; //不报异常,说明认证成功!跳转到首页
} catch (UnknownAccountException e) {
model.addAttribute("msg","用户名不存在");
return "login";
} catch (IncorrectCredentialsException e) {
model.addAttribute("msg","密码错误");
return "login";
}
}
@RequestMapping("/noAuth")
@ResponseBody
public String noAuth(){
return "未经授权不能访问该页面";
}
- 由于该示例用的是thymeleaf,所以导入了thymeleaf和shiro的整合包,就可以在模板中使用shiro的权限来控制一些菜单的展示和隐藏了
使用前提,shiro的配置类中要注入一个bean
//配置ShiroDialect:方言,用于 thymeleaf 和 shiro 标签配合使用
@Bean
public ShiroDialect getShiroDialect() {
return new ShiroDialect();
}
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-shiro" xmlns:shiro="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>首页</h1>
<p th:text="${msg}"></p>
<p th:if="${session.loginUser==null}">
<a th:href="@{/toLogin}">登录</a>
</p>
使用shiro的权限标签来控制菜单的显示与隐藏
<div shiro:hasPermission="user:add">
<a th:href="@{/user/add}">add</a>
</div>
|
<div shiro:hasPermission="user:update">
<a th:href="@{/user/update}">update</a>
</div>
</body>
</html>
以上案例只是Shiro的一个简单实现,Shiro中的东西远不止这些,实际开发中的业务逻辑是很复杂的。
五、什么是Spring Security?
Spring Security是一个基于Spring框架的功能强大且高度可定制的身份验证和访问控制框架。它实际上是保护基于spring的应用程序的标准。
安全框架一般来说要解决的最大的问题就是认证和授权。针对这两种情况,Spring Security 框架都有很好的支持。在用户认证方面,Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID和 LDAP 等。在用户授权方面,Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),可以对应用中的领域对象进行细粒度的控制。(相较于Shiro,Spring Security学习起来会稍微难一点,当然这是个人观点了)
六、SpringBoot中集成Spring Security
- 导入依赖
<!--导入Spring Security依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--thymeleaf与security整合包-->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<!--springboot web 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
- 建立一个Controller,和对应的访问页面
@Controller
public class RouterController {
@RequestMapping({"/", "/index"})
public String index() {
return "index";
}
@RequestMapping("/toLogin")
public String toLogin() {
return "views/login";
}
@RequestMapping("/level1/{id}")
public String level1(@PathVariable("id") int id) {
return "views/level1/" + id;
}
@RequestMapping("/level2/{id}")
public String level2(@PathVariable("id") int id) {
return "views/level2/" + id;
}
@RequestMapping("/level3/{id}")
public String level3(@PathVariable("id") int id) {
return "views/level3/" + id;
}
}
- 编写spring security配置文件
@EnableWebSecurity // 开启Security功能
public class SecurityConfig extends WebSecurityConfigurerAdapter{
// 权限
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/").permitAll()
.antMatchers("/level1/**").hasRole("vip1")
.antMatchers("/level2/**").hasRole("vip2")
.antMatchers("/level3/**").hasRole("vip3");
// 下面这是最简单登录和注销的配置,使用的都是security默认的登录和注销页面
//http.formLogin();//配置登录 默认登录页面/login
//http.logout();// 配置注销 默认注销页面/logout
/*http.formLogin()
.usernameParameter("username")
.passwordParameter("password")
.loginPage("/toLogin") // loginPage 定制登录页面
.loginProcessingUrl("/login");// loginProcessingUrl 用户名密码校验逻辑路径*/
http.formLogin().loginPage("/toLogin");// 若不配置loginProcessingUrl,则loginProcessingUrl默认和loginPage路径一样
http.csrf().disable();//关闭csrf:跨站请求伪造,不关闭的话默认只能通过post请求来注销,而我们页面上的注销按钮就是一个get请求的/logout跳转路径
http.logout().logoutSuccessUrl("/");//设置注销后跳转的页面
//记住我,就是保持登录状态,浏览器关闭再打开仍保持登录状态
http.rememberMe().rememberMeParameter("rememberCheck");
}
// 认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 账号和密码应该从数据库查,这里为了简单示例就直接把账号密码写到内存里
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("admin").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1")
.and()
.withUser("hhl").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2")
.and()
.withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3");
}
}
注意的事项:
- spring security 有默认的登录页面和注销页面
- 认证逻辑方法中的密码必须加密传输
- loginProcessingUrl是认证的逻辑判断路径,默认是/login