SpringBoot+SSM+Shiro+Thymeleaf 实现权限认证
一、SpringBoot+SSM+Shiro+Thymeleaf 实现权限认证
1.1 Apache Shiro 概述
(1)Apache Shiro 是ASF旗下的一款开源软件(Shiro发音为“shee-roh”,日语“堡垒(Castle)”的意思),提供的一个强大而灵活的安全框架。
(2)Apache Shiro提供了认证、授权、加密和会话管理功能,将复杂的问题隐藏起来,提供清晰直观的API使开发者可以很轻松地开发自己的程序安全代码。并且在实现此目标时无须依赖第三方的框架、容器或服务,当然也能做到与这些环境的整合,使其在任何环境下都可拿来使用。
(3)Shiro 将目标集中于Shiro开发团队所称的四大安全基石-认证(Authentication)、授权(Authorization)、会话管理(Session Management)和加密(Cryptography):
- 认证(Authentication):用户身份识别。有时可看作为“登录(login)”,它是用户证明自己是谁的一个行为。
- 授权(Authorization):访问控制过程,好比决定“认证(who)”可以访问“什么(what)”.
- 会话管理(SessionManagement):管理用户的会话(sessions),甚至在没有WEB或EJB容器的环境中。管理用户与时间相关的状态。
- 加密(Cryptography):使用加密算法保护数据更加安全,防止数据被偷窥。
1.2 Shiro 重要组件
- Subject:即"用户",外部应用都是和 Subject 进行交互的,subject 记录了当前操作用户,将用户的概念理解为当前操作的主体,可能是一个通过浏览器请求的用户,也可能是一个运行的程序。 Subject 在 shiro 中是一个接口,接口中定义了很多认证授权相关的方法,外部程序通过 subject 进行认证授权,而 subject 是通过 SecurityManager 安全管理器进行认证授权(Subject 相当于 SecurityManager 的门面)。
- SecurityManager:即安全管理器,它是 shiro 的核心,负责对所有的 subject 进行安全管理。通过 SecurityManager 可以完成 subject 的认证、授权等。
- Authentication:是一个对用户进行身份验证(登录)的组件。
- Authorization:即授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限。就是用来判断是否有权限,授权,本质就是访问控制,控制哪些URL可以访问.
- Realm:即领域,用于封装身份认证操作和授权操作,如果用户身份数据在数据库那么 realm 就需要从数据库获取用户身份信息。
在使用 Shiro 之前首先要明确的 Shiro 工作内容,Shiro 只负责对用户进行身份认证和权限验证,并不负责权限的管理,也就是说网页中的按钮是否显示、系统中有哪些角色、用户拥有什么角色、每个角色对应的权限有哪些,这些都需要我们自己来实现,换句话说 Shiro 只能利用现有的数据进行工作,而不能对数据库的数据进行修改。
1.3 使用 Shiro 实现认证流程图
1.4 使用SpringBoot+SSM+Shiro+Thymeleaf 实现身份认证步骤
(1)新建 SpringBoot 项目
新建 SpringBoot 项目,和以前一样在 src/main 下新建 resources 文件夹,resources 文件夹下新建 static 和 templates 文件夹以及application.properties配置文件。
项目结构如下:
(2)引入项目依赖
<!-- 定义公共资源版本 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.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>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 热部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
<scope>test</scope>
</dependency>
<!-- 对JDBC数据库的支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--数据库相关 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--mybatis的支持 包含mybatis的包和mybatis-spring插件包 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
<!--c3p0的依赖 -->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>
<!--shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!-- 模板引擎 Thymeleaf 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!-- 没有该配置,devtools 不生效 -->
<fork>true</fork>
</configuration>
</plugin>
</plugins>
</build>
(3)在application.properties文件中配置各项信息
#配置项目访问根路径
#server.servlet.context-path=/spring-boot-demo
#配置tomcat监听的端口,如果设置为80端口,在浏览器访问服务器时可以省略端口号
server.port=80
#spring相关配置
#静态资源映射
spring.resources.static-locations=classpath:/static/, classpath:/templates/
#数据源配置
#jdbc
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/ebuy?useUnicode=true&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=
#c3p0连接池
spring.datasource.type=com.mchange.v2.c3p0.ComboPooledDataSource
#mybatis配置
mybatis.mapperLocations=classpath:/mapper/*Mapper.xml
mybatis.type-aliases-package=com.woniu.entity
#thymeleaf配置
spring.thymeleaf.cache=false
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
(4)使用 mybatis 反向工程生成相关类和映射文件,定义业务层,在业务层中定义方法,业务方法的目的是通过用户名查询用户信息和用户角色信息和角色权限信息,角色信息和权限信息是为了在授权操作时使用,便于操作我们可以在User类中封装Role类型集合,Role类中封装 Permission类的集合,然后使用 mybatis 的一对多关联映射进行查询。
业务层主要代码:
@Service
public class UserServiceImp implements UserService{
@Resources
private UserMapper userMapper;
public User selectUserInfo(String username) throws Exception{
return userMapper.selectUserInfo(username);
}
}
数据层接口方法:
User selectUserInfo(String username);
映射文件查询标签:
<select id="selectUserInfo" parameterType="string resultMap="userInfoMap">
select u.*,r.rid,r.name rname,p.pid,p.resource from `user` u
inner join userRole ur on u.uid = uid
inner join role r on ur.rid = r.rid
inner join rp on ur.rid = rp.rid
inner join permission p on rp.rid = p.pid
where u.username = #{username};
</select>
映射文件中的自定义结果集:
<resultMap id="BaseResultMap" type="com.store.entity.Users">
<id column="uid" jdbcType="INTEGER" property="uid" />
<result column="username" jdbcType="VARCHAR" property="username" />
<result column="password" jdbcType="VARCHAR" property="password" />
<result column="phone" jdbcType="VARCHAR" property="phone" />
<result column="sex" jdbcType="INTEGER" property="sex" />
<result column="age" jdbcType="INTEGER" property="age" />
<result column="token" jdbcType="VARCHAR" property="token" />
<collection property="roles" ofType="com.store.entity.Roles">
<id column="rid" jdbcType="INTEGER" property="rid" />
<result column="rname" jdbcType="VARCHAR" property="rname" />
<collection property="permissions" ofType="com.store.entity.Permission">
<id column="pid" jdbcType="INTEGER" property="pid" />
<result column="resource" jdbcType="VARCHAR" property="resource" />
</collection>
</collection>
</resultMap>
(5)新建 Realm 类封装认证操作和授权操作,主要代码如下:
- 继承 AuthorizingRealm 类,重写方法
- 重新的两个方法,doGetAuthenticationInfo方法用于身份认证,doGetAuthorizationInfo方法用于进行授权操作。
- 在身份认证方法中,从参数token中取出用户名调用业务层的方法获取用户对象,如果用户存在则返回用户认证信息,交给Shiro去进行密码比对,如果用户不存在则直接返回null,Shiro将会抛出用户名不存在的异常,查询到用户信息可以将用户信息保存到Session中。
public class UsersRealm extends AuthorizingRealm{
@Resource
private LoginAndRegistService lars;
//授权方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
//认证方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
try {
Users users = lars.selectByUserName((String)token.getPrincipal());
if(users != null) {
//如果用户对象不为空,则封装为一个AuthenticationInfo对象并返回
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(users, users.getPassword(), getName());
return info;
}
} catch (Exception e) {
e.printStackTrace();
throw new AuthenticationException("服务器异常");
}
return null;
}
}
大家这里可能会有个疑惑,UserRealm 类上为什么没有添加@Component
注解,按照 Spring 的规则只有将对象交给Spring容器来管理,Spring才会完成依赖注入,这里不添加@Component
注解,那注入能成功吗?事实上我们会在另外一个位置用另外一种方式将UserRealm对象交给Spring容器来管理。咱们接着往下看。
(6)初始化 Shiro,初始化 Shiro 的 Realm、SecurityManager、和过滤器工厂对象。
- Realm:我们自定义的 UserReaml,该对象在此处进行初始化并交给Spring容器来管理
- SecurityManager:安全管理器,用于管理所有的认证信息。
- ShiroFilterFactoryBean:过滤器工厂对象,Shiro依赖过滤器多层的过滤器来实现身份认证判断,权限校验等,通过ShiroFilterFactoryBean可以规定过滤器的执行顺序和地址匹配规则。
代码:
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.shiro.mgt.SecurityManager;
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;
@Configuration
public class ShiroConfig {
// 初始化Realm
@Bean
public UserRealm initRealm() {
UserRealm realm = new UserRealm();
return realm;
}
// 加载安全管理器
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(initRealm());
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//注册securityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 拦截器+配置登录和登录成功之后的url
//LinkHashMap是有序的,shiro会根据添加的顺序进行拦截,匹配到过滤器后就执行该过滤器不会在继续向下查找过滤器
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
//配置不会被拦截地址规则
//anon:所有的url都可以不登陆的情况下访问
//authc:所有url都必须认证通过才可以访问
filterChainDefinitionMap.put("/css/**","anon");
filterChainDefinitionMap.put("/js/**","anon");
filterChainDefinitionMap.put("/fonts/**","anon");
filterChainDefinitionMap.put("/images/**","anon");
filterChainDefinitionMap.put("/login.html", "anon");
filterChainDefinitionMap.put("/login", "anon");
//如果不满足上方所有的规则 则需要进行登录验证
filterChainDefinitionMap.put("/**", "authc");
//未登录时重定向的网页地址
shiroFilterFactoryBean.setLoginUrl("/login.html");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
//返回
return shiroFilterFactoryBean;
}
}
核心代码讲解:
@Configuration
public class ShiroConfig {
SpringBoot 提供的注解,添加了该注解的类将作为 Spring 的配置器来使用,该类中所有添加了@Bean注解的方法所返回的数据都会交给 Spring 容器来管理。
// 初始化Realm
@Bean
public UserRealm initRealm() {
UserRealm realm = new UserRealm();
return realm;
}
初始化Realm,该对象需要交给SecurityManager来管理,SecurityManager需要使用其中定义的认证和授权方法。
// 加载安全管理器
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(initRealm());
return securityManager;
}
初始化 SecurityManager,将Realm交给它来管理。注意由于在java.lang包下也有一个SecurityManager类,所以我们使用时必须手动导包:import org.apache.shiro.mgt.SecurityManager;
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//注册securityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 拦截器+配置登录和登录成功之后的url
//LinkHashMap是有序的,shiro会根据添加的顺序进行拦截,匹配到过滤器后就执行该过滤器不会在继续向下查找过滤器
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
//配置不会被拦截地址规则
//anon:所有的url都可以不登陆的情况下访问
//authc:所有url都必须认证通过才可以访问
filterChainDefinitionMap.put("/css/**","anon");
filterChainDefinitionMap.put("/js/**","anon");
filterChainDefinitionMap.put("/fonts/**","anon");
filterChainDefinitionMap.put("/images/**","anon");
filterChainDefinitionMap.put("/login.html", "anon");
filterChainDefinitionMap.put("/login", "anon");
//如果不满足上方所有的规则 则需要进行登录验证
filterChainDefinitionMap.put("/**", "authc");
//未登录时重定向的网页地址
shiroFilterFactoryBean.setLoginUrl("/login.html");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
//返回
return shiroFilterFactoryBean;
}
初始化过滤器工厂,由于 SecurityManager 中封装了 Realm,Realm中又封装了认证方法,在过滤器中要调用认证方法,所以讲 SecurityManager 注册到工厂类中。
同时定义一个 LinkedHashMap,LinkedHashMap 是一个有序键值对,通过该键值对来保存过滤器匹配规则,anon是匿名过滤器,用来对一些请求放行,例如静态资源,登录页面,登录请求,注册页面,注册请求。authc过滤器必须在登录时才能访问,如果没有登录(SecurityManager中没有用户的认证信息),则会重定向到指定页面,要注意authc过滤器一定要写在最后,不然会导致静态资源无法访问,shiroFilterFactoryBean.setLoginUrl("/login.html");
用来配置未登录时的跳转页面。
(7)新建控制层 UserController,定义方法处理登录请求。
- 封装
UsernamePasswordToken
对象,该对象用于保存用户登录的用户名和密码 - 使用
SecurityUtils.getSubject()
获取主体对象,只有在登录成功后才会在Shiro中保存用户主体(登录成功后在Shiro中保存的用户信息) - 判断主体是否为空,如果不为空说明已经登录过,就不需要执行登录认证,直接转发到首页。
- 判断主体为空,说明当前还未有用户登录,就需要执行登录认证。在执行登录认证的过程中,Shiro会将登录结果以异常的方式抛出,用户名不存在和密码错误都会抛出异常,如果登陆成功将不会抛出异常,我们在控制层中直接返回登录成功后的逻辑视图即可,那么登录失败的响应在何处进行呢?既然是抛出了异常,那么我们完全可以定义一个全局的异常处理器,针对异常种类进行判断,然后来确定响应的逻辑视图信息。
控制层主要代码:
@Controller
public class UsersController {
@RequestMapping("login")
public ModelAndView login(String username, String password) throws Exception{
ModelAndView mav = new ModelAndView();
mav.setViewName("index");
mav.addObject("username", username);
//封装用户名和密码
UsernamePasswordToken token = new UsernamePasswordToken(String username, String password);
//创建subject 实例
Subject subject = SecurityUtils.getSubject();
//判断当前的subject是否登录
if(subject.isAuthenticated() == false){
//执行shiro的登录验证,最终会执行到Realm的认证方法
subjcet.login(token);
}
return mav;
}
}
全局异常处理器代码:
@Component
public class GolbalExceptionResolver implements HandlerExceptionResolver{
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
ModelAndView mav = new ModelAndView();
ex.printStackTrace();
if(ex instanceof UnknownAccountException) {
mav.setViewName("login");
mav.addObject("message", "该用户名不存在,请重新输入");
}else if(ex instanceof IncorrectCredentialsException) {
mav.setViewName("login");
mav.addObject("message", "用户名或密码错误,请重新输入");
}else if(ex instanceof UnauthorizedException) {
mav.setViewName("error");
mav.addObject("message", "没有访问权限?点击开通VIP立即获取>>");
}
else {
mav.setViewName("error");
mav.addObject("message", "条子来了");
}
return mav;
}
}
(8)编写首页和登录网页,测试登录认证。
1.5 添加权限验证模块步骤
(1)将用户拥有的角色名称和权限资源名称添加到Shiro的用户权限信息中,在UserRealm的doGetAuthorizationInfo方法中,取出登录时保存的用户信息,循环将角色和权限一起添加到Shiro的权限信息中。
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 获取登录用户
User user = (User) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
System.out.println("进入权限认证");
try { // 查询用户名称
for (Role role : user.getRoles()) {
// 添加角色
simpleAuthorizationInfo.addRole(role.getName());
for (Permission permission : role.getPermissions()) {
// 添加权限
simpleAuthorizationInfo.addStringPermission(permission.getResource());
}
}
} catch (Exception e) {
e.printStackTrace();
}
// 添加角色和权限
return simpleAuthorizationInfo;
}
每次用户发送请求时,Shiro都会执行该方法来获取用户的权限,进行判定。
(2)在ShiroConfig类中,添加两个Bean,用于扫描Shiro的注解和使用SpringAOP来完成权限校验。如果不添加这两个Bean,Shiro将无法使用注解的方式完成权限验证。
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor advisor=new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager());
return advisor;
}
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator app=new DefaultAdvisorAutoProxyCreator();
app.setProxyTargetClass(true);
return app;
}
(3)在控制层的方法上添加@RequiresPermissions(“insert”)注解,表示要执行该方法必须拥有insert权限。insert对应的是数据库中新增用户操作的resource值。只所以不使用权限名称进行判断,是因为Shiro的编码方法不支持中文。而且resource和请求地址是一致的也利于记忆和书写。
@RequestMapping("insert")
@RequiresPermissions("insert")
public ModelAndView insert(User user){
return new ModelAndView("success");
}
(4)当用户发送 insert 请求时,基于 SpringAOP 的前置通知,会去判断当前用户的权限信息中是否拥有 insert 权限,如果拥有则执行方法,如果没有该权限,则会抛出UnauthorizedException
异常,为了捕获这种异常并响应到权限不足的提示页面,需要在全局的异常处理器中,对该类异常进行处理。
}else id(ex instanceof UnauthorizedException){
mv.setViewName("unpermission")
}
需要自定义unpermission.html
页面,提示权限不足的信息。
(5)因为我们在控制层,添加了禁止重复登陆的判断,所以当我们使用另外一个用户登录时,事实上并不会重新进行登录,我们会发现Shiro认证的信息仍然是第一次登录的用户信息,所以我们需要为项目提供登出的功能。只需要在ShiroConfig中配置一个登出过滤器,将/logout请求交给Shiro的登出过滤器来处理,在登出过滤器中Shiro会将已经认证的信息删除,并自动重定向到登录页面。
filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/***", "authc");
//未登录时和登出时重定向的网页网址
shiroFilterFactoryBean.setLoginUrl("login.html");