综合概述
Shiro是Apache旗下的一个开源项目,它是一个非常易用的安全框架,提供了包括认证、授权、加密、会话管理等功能,与Spring Security一样属基于权限的安全框架,但是与Spring Security 相比,Shiro使用了比较简单易懂易于使用的授权方式。Shiro属于轻量级框架,相对于Spring Security简单很多,并没有security那么复杂。
优势特点
它是一个功能强大、灵活的、优秀的、开源的安全框架。
它可以胜任身份验证、授权、企业会话管理和加密等工作。
它易于使用和理解,与Spring Security相比,入门门槛低。
主要功能
验证用户身份
用户访问权限控制
支持单点登录(SSO)功能
可以响应认证、访问控制,或Session事件
支持提供“Remember Me”服务
.......
框架体系
Shiro 的整体框架大致如下图所示(图片来自互联网):
Authentication(认证), Authorization(授权), Session Management(会话管理), Cryptography(加密)代表Shiro应用安全的四大基石。
它们分别是:
Authentication(认证):用户身份识别,通常被称为用户“登录”。
Authorization(授权):访问控制。比如某个用户是否具有某个操作的使用权限。
Session Management(会话管理):特定于用户的会话管理,甚至在非web 应用程序。
Cryptography(加密):在对数据源使用加密算法加密的同时,保证易于使用。
除此之外,还有其他的功能来支持和加强这些不同应用环境下安全领域的关注点。
特别是对以下的功能支持:
Web支持:Shiro 提供的 web 支持 api ,可以很轻松的保护 web 应用程序的安全。
缓存:缓存是 Apache Shiro 保证安全操作快速、高效的重要手段。
并发:Apache Shiro 支持多线程应用程序的并发特性。
测试:支持单元测试和集成测试,确保代码和预想的一样安全。
“Run As”:这个功能允许用户在许可的前提下假设另一个用户的身份。
“Remember Me”:跨 session 记录用户的身份,只有在强制需要时才需要登录。
主要流程
在概念层,Shiro 架构包含三个主要的理念:Subject, SecurityManager 和 Realm。下面的图展示了这些组件如何相互作用,我们将在下面依次对其进行描述。
Shiro执行流程图(图片来自互联网)
三个主要理念:
Subject:代表当前用户,Subject 可以是一个人,也可以是第三方服务、守护进程帐户、时钟守护任务或者其它当前和软件交互的任何事件。
SecurityManager:管理所有Subject,SecurityManager 是 Shiro 架构的核心,配合内部安全组件共同组成安全伞。
Realms:用于进行权限信息的验证,我们自己实现。Realm 本质上是一个特定的安全 DAO:它封装与数据源连接的细节,得到Shiro 所需的相关的数据。在配置 Shiro 的时候,你必须指定至少一个Realm 来实现认证(authentication)和/或授权(authorization)。
我们需要实现Realms的Authentication 和 Authorization。其中 Authentication 是用来验证用户身份,Authorization 是授权访问控制,用于对用户进行的操作授权,证明该用户是否允许进行当前操作,如访问某个链接,某个资源文件等。
实现案例
接下来,我们就通过一个具体的案例,来讲解如何进行Shiro的整合,然后借助Shiro实现登录认证和访问控制。
生成项目模板
为方便我们初始化项目,Spring Boot给我们提供一个项目模板生成网站。
2. 根据页面提示,选择构建工具,开发语言,项目信息等。
3. 点击 Generate the project,生成项目模板,生成之后会将压缩包下载到本地。
4. 使用IDE导入项目,我这里使用Eclipse,通过导入Maven项目的方式导入。
添加相关依赖
清理掉不需要的测试类及测试依赖,添加 Maven 相关依赖,这里需要添加上WEB、Swagger、JPA和Shiro的依赖,Swagger的添加是为了方便接口测试。
pom.xml
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.1.5.RELEASE
com.louis.springboot
demo
0.0.1-SNAPSHOT
demo
Demo project for Spring Boot
1.8
org.springframework.boot
spring-boot-starter-web
io.springfox
springfox-swagger2
2.9.2
io.springfox
springfox-swagger-ui
2.9.2
org.springframework.boot
spring-boot-starter-data-jpa
mysql
mysql-connector-java
org.apache.shiro
shiro-spring
1.4.1
org.springframework.boot
spring-boot-maven-plugin
src/main/java
**/sqlmap/*.xml
false
src/main/resources
**/*.*
true
添加相关配置
1.添加数据源和jpa相关配置
将application.properties文件改名为application.yml ,并在其中添加MySQL数据源连接信息。
注意:
这里需要首先创建一个MySQL数据库,并输入自己的用户名和密码。这里的数据库是springboot。
另外,如果你使用的是MySQL 5.x及以前版本,驱动配置driverClassName是com.mysql.jdbc.Driver。
application.yml
server:
port:8080spring:
datasource:
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/springboot?useUnicode=true&zeroDateTimeBehavior=convertToNull&autoReconnect=true&characterEncoding=utf-8
username: root
password:123456jpa:
show-sql: true# 默认false,在日志里显示执行的sql语句
database: mysql
hibernate.ddl-auto: update #指定为update,每次启动项目检测表结构有变化的时候会新增字段,表不存在时会新建,如果指定create,则每次启动项目都会清空数据并删除表,再新建
properties.hibernate.dialect: org.hibernate.dialect.MySQL5Dialect
database-platform: org.hibernate.dialect.MySQL5Dialect
hibernate:
naming:implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl #指定jpa的自动表生成策略,驼峰自动映射为下划线格式
#physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
2. 添加swagger 配置
添加一个swagger 配置类,在工程下新建 config 包并添加一个 SwaggerConfig 配置类。
SwaggerConfig.java
package com.louis.springboot.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2public classSwaggerConfig {
@BeanpublicDocket createRestApi(){return newDocket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any()).build();
}privateApiInfo apiInfo(){return newApiInfoBuilder()
.title("SpringBoot API Doc")
.description("This is a restful api document of Spring Boot.")
.version("1.0")
.build();
}
}
编写业务代码
添加一个用户类User,包含用户名和密码,用来进行登录认证,另外用户可以拥有角色。
User.java
package com.louis.springboot.demo.model;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
@Entitypublic classUser {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)privateLong id;
@Column(unique= true)privateString name;privateString password;
@OneToMany(cascade= CascadeType.ALL,mappedBy = "user")private Listroles;publicLong getId() {returnid;
}public voidsetId(Long id) {this.id =id;
}publicString getName() {returnname;
}public voidsetName(String name) {this.name =name;
}public ListgetRoles() {returnroles;
}public void setRoles(Listroles) {this.roles =roles;
}publicString getPassword() {returnpassword;
}public voidsetPassword(String password) {this.password =password;
}
}
添加一个角色类Role,表示用户角色,角色拥有可操作的权限集合。
Role.java
package com.louis.springboot.demo.model;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
@Entitypublic classRole {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)privateLong id;privateString roleName;
@ManyToOne(fetch=FetchType.EAGER)privateUser user;
@OneToMany(cascade= CascadeType.ALL,mappedBy = "role")private Listpermissions;publicLong getId() {returnid;
}public voidsetId(Long id) {this.id =id;
}publicString getRoleName() {returnroleName;
}public voidsetRoleName(String roleName) {this.roleName =roleName;
}publicUser getUser() {returnuser;
}public voidsetUser(User user) {this.user =user;
}public ListgetPermissions() {returnpermissions;
}public void setPermissions(Listpermissions) {this.permissions =permissions;
}
}
添加一个权限类Permission,表示资源访问权限。
Permission.java
package com.louis.springboot.demo.model;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
@Entitypublic classPermission {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)privateLong id;privateString permission;
@ManyToOne(fetch=FetchType.EAGER)privateRole role;publicLong getId() {returnid;
}public voidsetId(Long id) {this.id =id;
}publicString getPermission() {returnpermission;
}public voidsetPermission(String permission) {this.permission =permission;
}publicRole getRole() {returnrole;
}public voidsetRole(Role role) {this.role =role;
}
}
添加一个DAO基础接口,用来被其他DAO继承。
BaseDao.java
package com.louis.springboot.demo.dao;
import java.io.Serializable;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.PagingAndSortingRepository;
@NoRepositoryBeanpublic interface BaseDaoextends PagingAndSortingRepository, JpaSpecificationExecutor{
}
添加一个UserDao,用来操作用户信息。
UserDao.java
package com.louis.springboot.demo.dao;
import com.louis.springboot.demo.model.User;public interface UserDao extends BaseDao{
User findByName(String name);
}
添加一个RoleDao,用来操作角色信息。
RoleDao.java
package com.louis.springboot.demo.dao;
import com.louis.springboot.demo.model.Role;public interface RoleDao extends BaseDao{
}
添加一个LoginService服务接口。
LoginService.java
package com.louis.springboot.demo.service;
import com.louis.springboot.demo.model.Role;
import com.louis.springboot.demo.model.User;public interfaceLoginService {
User addUser(User user);
Role addRole(Role role);
User findByName(String name);
}
添加一个LoginServiceImpl,实现服务功能,这里为了方便,在插入角色的时候会默认设置其权限。
LoginServiceImpl.java
package com.louis.springboot.demo.service.impl;
import java.util.ArrayList;
import java.util.List;
import javax.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.louis.springboot.demo.dao.RoleDao;
import com.louis.springboot.demo.dao.UserDao;
import com.louis.springboot.demo.model.Permission;
import com.louis.springboot.demo.model.Role;
import com.louis.springboot.demo.model.User;
import com.louis.springboot.demo.service.LoginService;
@Service
@Transactionalpublic classLoginServiceImpl implements LoginService {
@AutowiredprivateUserDao userDao;
@AutowiredprivateRoleDao roleDao;//添加用户
@OverridepublicUser addUser(User user) {
userDao.save(user);returnuser;
}//添加角色
@OverridepublicRole addRole(Role role) {
User user=userDao.findByName(role.getUser().getName());
role.setUser(user);
Permission permission1= newPermission();
permission1.setPermission("create");
permission1.setRole(role);
Permission permission2= newPermission();
permission2.setPermission("update");
permission2.setRole(role);
List permissions = new ArrayList();
permissions.add(permission1);
permissions.add(permission2);
role.setPermissions(permissions);
roleDao.save(role);returnrole;
}//查询用户通过用户名
@OverridepublicUser findByName(String name) {returnuserDao.findByName(name);
}
}
添加一个登录控制器,编写相关的接口。create接口添加了@RequiresPermissions("create"),用于进行权限注解测试。
LoginController.java
package com.louis.springboot.demo.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.louis.springboot.demo.model.Role;
import com.louis.springboot.demo.model.User;
import com.louis.springboot.demo.service.LoginService;
@RestControllerpublic classLoginController {
@AutowiredprivateLoginService loginService;/**
* POST登录
* @param map
* @return*/@PostMapping(value= "/login")publicString login(@RequestBody User user) {//添加用户认证信息
UsernamePasswordToken usernamePasswordToken = newUsernamePasswordToken(user.getName(), user.getPassword());//进行验证,这里可以捕获异常,然后返回对应信息
SecurityUtils.getSubject().login(usernamePasswordToken);return "login ok!";
}/**
* 添加用户
* @param user
* @return*/@PostMapping(value= "/addUser")publicString addUser(@RequestBody User user) {
user=loginService.addUser(user);return "addUser is ok! \n" +user;
}/**
* 添加角色
* @param role
* @return*/@PostMapping(value= "/addRole")publicString addRole(@RequestBody Role role) {
role=loginService.addRole(role);return "addRole is ok! \n" +role;
}/**
* 注解的使用
* @return*/@RequiresRoles("admin")
@RequiresPermissions("create")
@GetMapping(value= "/create")publicString create() {return "Create success!";
}
@GetMapping(value= "/index")publicString index() {return "index page!";
}
@GetMapping(value= "/error")publicString error() {return "error page!";
}/**
* 退出的时候是get请求,主要是用于退出
* @return*/@GetMapping(value= "/login")publicString login() {return "login";
}
@GetMapping(value= "/logout")publicString logout() {return "logout";
}
}
添加一个MyShiroRealm并继承AuthorizingRealm,实现其中的两个方法。
doGetAuthenticationInfo:实现用户认证,通过服务加载用户信息并构造认证对象返回。
doGetAuthorizationInfo:实现权限认证,通过服务加载用户角色和权限信息设置进去。
MyShiroRealm.java
package com.louis.springboot.demo.config;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import com.louis.springboot.demo.model.Permission;
import com.louis.springboot.demo.model.Role;
import com.louis.springboot.demo.model.User;
import com.louis.springboot.demo.service.LoginService;/**
* 实现AuthorizingRealm接口用户用户认证
* @author Louis
* @date Jun 20, 2019*/
public classMyShiroRealm extends AuthorizingRealm {
@AutowiredprivateLoginService loginService;/**
* 用户认证*/@OverrideprotectedAuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
throws AuthenticationException {//加这一步的目的是在Post请求的时候会先进认证,然后在到请求
if (authenticationToken.getPrincipal() == null) {return null;
}//获取用户信息
String name =authenticationToken.getPrincipal().toString();
User user=loginService.findByName(name);if (user == null) {//这里返回后会报出对应异常
return null;
}else{//这里验证authenticationToken和simpleAuthenticationInfo的信息
SimpleAuthenticationInfo simpleAuthenticationInfo = newSimpleAuthenticationInfo(name,
user.getPassword().toString(), getName());returnsimpleAuthenticationInfo;
}
}/**
* 角色权限和对应权限添加*/@OverrideprotectedAuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {//获取登录用户名
String name =(String) principalCollection.getPrimaryPrincipal();//查询用户名称
User user =loginService.findByName(name);//添加角色和权限
SimpleAuthorizationInfo simpleAuthorizationInfo = newSimpleAuthorizationInfo();for(Role role : user.getRoles()) {//添加角色
simpleAuthorizationInfo.addRole(role.getRoleName());for(Permission permission : role.getPermissions()) {//添加权限
simpleAuthorizationInfo.addStringPermission(permission.getPermission());
}
}returnsimpleAuthorizationInfo;
}
}
添加一个Shiro配置类,主要配置路由的访问控制,以及注入自定义的认证器MyShiroRealm。
ShiroConfig.java
package com.louis.springboot.demo.config;
import java.util.HashMap;
import java.util.Map;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configurationpublic classShiroConfig {//将自己的验证方式加入容器
@BeanpublicMyShiroRealm myShiroRealm() {
MyShiroRealm myShiroRealm= newMyShiroRealm();returnmyShiroRealm;
}//权限管理,配置主要是Realm的管理认证
@BeanpublicSecurityManager securityManager() {
DefaultWebSecurityManager securityManager= newDefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());returnsecurityManager;
}//Filter工厂,设置对应的过滤条件和跳转条件
@BeanpublicShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean= newShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map filterMap = new HashMap();//登出
filterMap.put("/logout", "logout");//swagger
filterMap.put("/swagger**/**", "anon");
filterMap.put("/webjars/**", "anon");
filterMap.put("/v2/**", "anon");//对所有用户认证
filterMap.put("/**", "authc");//登录
shiroFilterFactoryBean.setLoginUrl("/login");//首页
shiroFilterFactoryBean.setSuccessUrl("/index");//错误页面,认证不通过跳转
shiroFilterFactoryBean.setUnauthorizedUrl("/error");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);returnshiroFilterFactoryBean;
}//加入注解的使用,不加入这个注解不生效
@BeanpublicAuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor= newAuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);returnauthorizationAttributeSourceAdvisor;
}
}
编译测试运行
1. 右键项目 -> Run as -> Maven install,开始执行Maven构建,第一次会下载Maven依赖,可能需要点时间,如果出现如下信息,就说明项目编译打包成功了。
2. 右键文件 DemoApplication.java -> Run as -> Java Application,开始启动应用,如果一开始数据库没有对应的表,在应用启动时会创建,我们可以通过控制台查看到对应的SQL语句。
4. 首先用MySQL客户端,打开数据库,往用户表里面插入一条记录,{id=1, name="admin", password="123"}。
然后试着用Swagger调用addUser往用户表插入一条记录。
{"id": 2,"name": "xiaoming","password": "123"}
结果返回"error page!",这是因为我们没有登录,还没有操作权限。
接着调用POST的login接口,输入以下用户信息进行登录。
{"name": "admin","password": "123"}
登录成功之后,返回“login ok!”信息。
再次调用addUser往用户表插入记录,发现记录已经可以成功插入了。
通过客户端工具我们也可以查看到记录已经插入进来了。
通过上面的测试,我们已经成功的验证了,受保护的接口需要在登录之后才允许访问。接下来我们来测试一下权限注解的效果,我们在create方法上加上了权限注解@RequiresPermissions("create"),表示用户需要拥有"create"的权限才能访问。
先尝试调用以下create接口,发现尽管我们已经登录了,依然因为没有权限返回了“error page!”。
然后我们调用addRole插入以下角色记录,这个角色关联了我们当前登录admin用户,且角色在创建时我们代码默认设置拥有了“create”权限。
{"id": 1,"roleName": "admin","user": {"name": "admin"}
}
如果执行正确的话,会返回如下信息,说明角色已经成功插入了。
然后我们再一次调用create接口,因为此刻admin用户拥有admin角色,而admin角色拥有“create”权限,所以已经具有接口访问权限了。
参考资料
相关导航
源码下载