在上一篇文章【Springboot整合Spring Security实现安全控制和权限方案】中,我们使用Spring Boot应用中集成了Spring Security,并实现了基本的认证与授权功能,同时也掌握了如何使用Thymeleaf与Spring Security结合来控制页面的访问权限。接下来,我们将探索另一种安全解决方案——集成Shiro,它将为我们提供另一套强大的安全防护机制,让我们在不同的场景下能够更加灵活地选择合适的安全框架来保护应用。
任务描述
任务要求
使用IDEA开发工具构建一个项目多模块工程。study-springboot-chapter15学习关于Springboot集成shiro实现安全认证和授权知识点
- 基于study-springboot工程,复制study-springboot-chapter15标准项目,坐标groupId(com.cbitedu)、artifactId(study-springboot-chapter15),其他默认
- 继承study-springboot工程依赖
- 引入Apache Shiro安全框架,实现系统认证和授权
- 持久化层使用JPA实现
任务收获
- Spring Boot中整合Apache Shiro框架
- 学会使用JUnit完成单元测试
- Apache shiro权限实现原理
- 掌握web开发原理
- 熟悉SpringMVC的基础配置
- JPA持久化技术的应用
任务准备
环境要求
- JDK1.8+
- MySQL8.0.27+
- Maven 3.6.1+
- IDEA/VSCode
数据库准备
-- ----------------------------
-- Table structure for t_sys_userinfo
-- ----------------------------
DROP TABLE IF EXISTS t_sys_userinfo;
CREATE TABLE t_sys_userinfo (
user_id varchar(32) NOT NULL,
username varchar(50) NOT NULL COMMENT '用户名',
password varchar(100) COMMENT '密码',
salt varchar(20) COMMENT '盐',
email varchar(100) COMMENT '邮箱',
mobile varchar(100) COMMENT '手机号',
status tinyint COMMENT '状态 0:禁用 1:正常',
create_user_id varchar(32) COMMENT '创建者ID',
create_time varchar(32) COMMENT '创建时间',
userimg varchar(255) COMMENT '用户头像',
zip varchar(10) COMMENT '邮政编码',
sort_num int COMMENT '排序号',
user_type varchar(10) COMMENT '用户类型',
post_id varchar(32) COMMENT '所属岗位',
sex varchar(4) COMMENT '性别',
USER_REALNAME varchar(50) COMMENT '真实姓名',
user_theme varchar(255) COMMENT '用户选择皮肤',
user_card varchar(18) COMMENT '身份证号码',
birthday varchar(20) COMMENT '出生年月',
native_place varchar(255) COMMENT '家庭住址',
nation varchar(255) COMMENT '民族',
update_user_id varchar(32) COMMENT '创建者ID',
update_time varchar(32) COMMENT '创建时间',
PRIMARY KEY (user_id) USING BTREE,
UNIQUE INDEX username(username ASC) USING BTREE
) COMMENT = '系统用户' ;
-- ----------------------------
-- Records of t_sys_userinfo
-- ----------------------------
INSERT INTO t_sys_userinfo VALUES ('1', 'admin', 'efc7b4b496e9db2e7a605db183f9d831', 'YzcmCZNvbXocrsz9dm8e', 'cc@bluefairy.com', '18929423839', 1, '1', '2016-11-11 11:11:11', NULL, NULL, NULL, NULL, NULL, NULL, '系统管理员', 'green', NULL, NULL, NULL, NULL, NULL, NULL);
工程目录要求
study-springboot-chapter15
源码地址
任务实施
在所有的开发的系统中,都必须做认证(authentication)和授权(authorization),以保证系统的安全性。
- authentication: 【认证】你要登录论坛,输入用户名张三,密码 1234,密码正确,证明你张三确实是张三,这就是 authentication。
- authorization:【授权】再一 check 用户张三是个版主,所以有权限加精删别人帖,这就是 authorization 。
所以简单来说:认证解决“你是谁”的问题,授权解决“你能做什么”的问题。
第一步:基于标准Springboot工程模板study-springboot-chapter00继承父工程,在pom.xml中加入所需的依赖:
<!-- jpa -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Shiro权限管理 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.9.1</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.9.1</version>
</dependency>
<!-- alibaba的druid数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.12</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
第二步:application.yml文件中注入Thymeleaf相关配置,默认只要我们把HTML页面放在classpath:/templates/
,thymeleaf就能自动渲染。
#服务配置
server:
port: 81
# #设置日志相关打印sql 语句
logging:
level:
com.cbitedu.springboot: debug
org.springframework.web: debug
#关闭运行日志图标(banner)
spring:
datasource:
url: jdbc:mysql://localhost:3306/platform?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
#Thymeleaf模板引擎相关配置
thymeleaf:
#prefix:指定模板所在的目录
prefix: classpath:/templates/
#check-tempate-location: 检查模板路径是否存在
check-template-location: true
#cache: 是否缓存,开发模式下设置为false,避免改了模板还要重启服务器,线上设置为true,可以提高性能。
cache: false
#suffix 配置模板后缀名
suffix: .html
encoding: UTF-8
mode: HTML5
devtools:
restart:
enabled: true #设置开启热部署
additional-paths: src/main/java #重启目录
# exclude: static/**,public/**,db/**,i18n/**,templates/** #排除文件(不重启项目)
第三步:创建实体类(com.cbitedu.springboot.entity.User)
package com.cbitedu.springboot.entity;
import lombok.Data;
import javax.persistence.*;
@Data
@Entity(name = "t_sys_userinfo")
public class User {
@Id
@GeneratedValue(strategy= GenerationType.AUTO)
private Long id;
@Column(name = "username")
private String userName;
@Column(name = "password")
private String userPwd;
}
持久化类(com.cbitedu.springboot.daoUserRepository )
package com.cbitedu.springboot.dao;
import com.cbitedu.springboot.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
User findByUserName(String userName);
}
服务类(接口类、实现类)UserService 接口
package com.cbitedu.springboot.service;
import com.cbitedu.springboot.entity.User;
public interface UserService {
User findByUserName(String userName);
}
package com.cbitedu.springboot.service.impl;
import com.cbitedu.springboot.dao.UserRepository;
import com.cbitedu.springboot.entity.User;
import com.cbitedu.springboot.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Override
public User findByUserName(String userName) {
return userRepository.findByUserName(userName);
}
}
第四步:Apache Shiro配置类UserRealm
package com.cbitedu.springboot.realm;
import com.cbitedu.springboot.entity.User;
import com.cbitedu.springboot.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
@Slf4j
public class UserRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
log.info("-------------------------------> 认证");
String userName = (String) token.getPrincipal();
User user = userService.findByUserName(userName);
if (user == null) {
throw new UnknownAccountException();
}
String salt = "sdfnegaf7gafj3nfdsfdsj9"; // 盐可以自定义
return new SimpleAuthenticationInfo(user, user.getUserPwd(), ByteSource.Util.bytes(salt), getName());
}
}
第五步:启用Apache Shiro配置类ShiroConfiguration
package com.cbitedu.springboot.config;
import com.cbitedu.springboot.realm.UserRealm;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
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.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.*;
@Slf4j
@Configuration
public class ShiroConfiguration {
/**
* ShiroFilterFactoryBean,是个factorybean,为了生成ShiroFilter。
* 它主要保存了三项数据,securityManager,filters,filterChainDefinitionManager。
* <p>
* ShiroFilterFactoryBean 处理拦截资源文件问题。
* 注意:单独一个ShiroFilterFactoryBean配置是或报错的,因为在初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager
* Filter Chain定义说明
* 1、一个URL可以配置多个Filter,使用逗号分隔
* 2、当设置多个过滤器时,全部验证通过,才视为通过
* 3、部分过滤器可指定参数,如perms,roles
*/
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
log.info("加载 shiro");
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
shiroFilterFactoryBean.setLoginUrl("/login");
shiroFilterFactoryBean.setSuccessUrl("/");
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
//拦截器.
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// 静态资源
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/**", "user");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//自定义单个realm.
securityManager.setRealm(getUserRealm());
return securityManager;
}
/**
* ShiroRealm,这是个自定义的认证类,继承自AuthorizingRealm,
* 负责用户的认证和权限的处理,可以参考JdbcRealm的实现。
*/
@Bean
public Realm getUserRealm() {
UserRealm userRealm = new UserRealm();
userRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return userRealm;
}
/**
* HashedCredentialsMatcher,这个类是为了对密码进行编码的,
* 防止密码在数据库里明码保存,当然在登陆认证的时候,
* 这个类也负责对form里输入的密码进行编码。
*/
/**
* 凭证匹配器
* (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
* 所以我们需要修改下doGetAuthenticationInfo中的代码;
* )
*
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
hashedCredentialsMatcher.setHashIterations(2);
return hashedCredentialsMatcher;
}
/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions)
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
*
* @return
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* AuthorizationAttributeSourceAdvisor,shiro里实现的Advisor类,
* 内部使用AopAllianceAnnotationsAuthorizingMethodInterceptor来拦截用以下注解的方法。
*/
/**
* 开启shiro aop注解支持.
* 使用代理方式;所以需要开启代码支持;
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* 负责org.apache.shiro.utils.Initializable类型bean的生命周期的,初始化和销毁。
*/
@Bean
public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
}
第六步,页面控制类 LoginController
package com.cbitedu.springboot.controller;
import com.cbitedu.springboot.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.DisabledAccountException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@Slf4j
@Controller
public class LoginController {
@GetMapping(value = {"/", "/index"})
public String index() {
return "login";
}
@GetMapping("/login")
public String login() {
return "login";
}
@PostMapping("/login")
public String login(User user, Model model) {
log.info("用户名;" + user.getUserName() + ", 密码:" + user.getUserPwd());
UsernamePasswordToken token = new UsernamePasswordToken(user.getUserName(), user.getUserPwd());
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
return "index";
} catch (DisabledAccountException e) {
model.addAttribute("message", "该用户未授权");
return "login";
} catch (UnknownAccountException e) {
model.addAttribute("message", "该用户不存在");
return "login";
} catch (IncorrectCredentialsException e) {
model.addAttribute("message", "密码错误");
return "login";
}
}
}
第七步:创建登录表单
我们在 resource/templates
目录下新建书籍列表页面 login.html
,index.html代码如下:
login.html代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0 ,user-scalable=no">
<title>登录</title>
<!--兼容处理,如果可能,调取ie高版本内核-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="stylesheet" href="css/login.css">
<link rel="stylesheet" href="iconfont/icon.css">
</head>
<body>
<div class="form-box">
<i class="iconfont icon-U"></i>
<form action="/login" method="post">
<h2>Login</h2>
<label for="user">姓名</label>
<!--点击laber获得input焦点,for绑定id-->
<input type="text" name="userName" id="user" value="admin" required />
<label for="password">密码</label>
<input type="password" name="userPwd" id="password" value="123456" required/>
<p>
<input type="checkbox" name="check" id="check">
<label for="check">记住密码</label>
</p>
<input type="submit" value="登录">
<p class="toregister">没有账号?<a href="#">去注册</a></p>
</form>
</div>
</body>
</html>
index.html源代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h2 style="color: green">登录成功</h2>
</body>
</html>
此时我们启动项目,然后访问 http://localhost:81/login ,账号:admin/123456
切换不同用户验证结果。
小结
Spring Data JPA
Spring Data JPA 是 Spring 基于 ORM 框架、JPA 规范的基础上封装的一套JPA应用框架,可使开发者用极简的代码即可实现对数据的访问和操作。它提供了包括增删改查等在内的常用功能,且易于扩展!学习并使用 Spring Data JPA 可以极大提高开发效率!
spring data jpa让我们解脱了DAO层的操作,基本上所有CRUD都可以依赖于它来实现
实验实训
1、重点学习Apache Shiro其他使用
2、熟练Spring Data JPA使用