目录
1. 添加mybatis-plus , 创建数据表,并添加数据
3. 我们创建自定义 MyUserDetailsServiceImpl
Spring Security 学习专栏
2. Spring Security 自定义认证管理器和讲解 (二)
一、概述
下面开始实战使用Spring Security 的实例查询数据库,依然依托上一篇的例子,并在此基础上调整。
通过数据库查询,存储用户和角色实现安全认证
在上一篇的例子中(Spring Security 自定义认证管理器和讲解),我们使用了内存用户角色来演示登录认证。但是实际项目我们肯定是通过数据库完成的。实际项目中,我们可能会有五张表:用户表,角色表,用户角色关联表,权限表,角色权限关联表。本例演示的意义在于:如果我们想在已有项目中增加Spring Security的话,就需要调整登录了。主要是自定义UserDetailsService
,此外,可能还需要处理密码的问题,因为Spring并不知道我们怎么加密用户登录密码的。这时,我们可能需要自定义PasswordEncoder(
Spring Security 接口详解 )
二、搭建自定义UserDetailsService
1. 添加mybatis-plus , 创建数据表,并添加数据
继续完善开篇的项目,现在给项目添加mybatis-plus和数据库驱动
,并使用MySQL数据库。因此在POM文件添加如下jar包
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1.tmp</version>
</dependency>
<!--Mysql数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.15</version>
</dependency>
在 application.yml文件中加入数据库连接信息:
spring:
#数据库连接配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://47.103.20.21:3307/zlp-mall?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
username: root
password: zxx1234xx
2. 创建数据库表结构
主表:sys_user,sys_role,sys_permission 三张表都是多对多的关系
关联关系表:sys_role_permission_relation, sys_user_role_relation
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`username` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名',
`password` varchar(150) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码',
`nickname` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户昵称',
`user_type` int(2) NOT NULL DEFAULT 1 COMMENT '用户类型 (0:管理员;1:普通用户)',
`status` int(2) NOT NULL DEFAULT 0 COMMENT '状态(0:正常,1:删除)',
`create_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`create_user` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建用户',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
`update_user` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '修改人',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 12 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '管理用户表' ROW_FORMAT = Dynamic;
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`role_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '名称',
`description` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '描述',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`role_status` int(1) NULL DEFAULT 1 COMMENT '启启用状态:0->启用 ; 1->禁用',
`sort` int(11) NULL DEFAULT 0,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '后台用户角色表' ROW_FORMAT = Dynamic;
DROP TABLE IF EXISTS `sys_permission`;
CREATE TABLE `sys_permission` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`pid` bigint(20) NULL DEFAULT NULL COMMENT '父级权限id',
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '名称',
`permission_value` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '权限值',
`icon` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '图标',
`type` int(1) NULL DEFAULT NULL COMMENT '权限类型:0->目录;1->菜单;2->按钮(接口绑定权限)',
`data_type` int(1) NULL DEFAULT 0 COMMENT '权限数据类型:0->查看全部;1->查看自己数据权限',
`url` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '前端资源路径',
`permission_status` int(1) NOT NULL DEFAULT 1 COMMENT '启用状态;0->启用;1->禁用',
`sort` int(4) NULL DEFAULT NULL COMMENT '排序',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`create_user` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建用户',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`update_user` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建用户',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_pid`(`pid`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 64 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '后台用户权限表' ROW_FORMAT = Dynamic;
DROP TABLE IF EXISTS `sys_user_role_relation`;
CREATE TABLE `sys_user_role_relation` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NULL DEFAULT NULL,
`role_id` bigint(20) NULL DEFAULT NULL,
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 21 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '后台用户和角色关系表' ROW_FORMAT = Dynamic;
DROP TABLE IF EXISTS `sys_role_permission_relation`;
CREATE TABLE `sys_role_permission_relation` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`role_id` bigint(20) NULL DEFAULT NULL,
`permission_id` bigint(20) NULL DEFAULT NULL,
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 793 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '后台用户角色和权限关系表' ROW_FORMAT = Dynamic;
3. 我们创建自定义 MyUserDetailsServiceImpl
前面我们提到过,UserDetailsService,Spring Security在认证过程中需要查找用户,会调用UserDetailsService的loadUserByUsername方法得到一个UserDetails,下面我们来实现他。代码如下:
@Service("userDetailsService")
@Slf4j(topic = "MyUserDetailsServiceImpl")
public class MyUserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("loadUserByUsername.req username={}",username);
// 1. 查询用户
User user = userService.loadUserByUsername(username);
if (Objects.isNull(user)) {
log.error("User [" + username + "] was not found in db");
throw new UsernameNotFoundException("User [" + username + "] was not found in db");
}
// 密码
String password = user.getPassword();
// 2. 设置权限角色信息
List<GrantedAuthority> grantedAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList
("ADMIN,ROLE_abc");
return new org.springframework.security.core.userdetails.User(username,password, grantedAuthorities);
}
}
4. SecurityConfig 配置
设置自定义的userDetailsService,密码采用PasswordEncoder密码解析器
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private MyAccessDeniedHandler accessDeniedHandler;
/**
* 认证管理器配置
* @param auth
* @date: 2021/3/11 17:39
* @return: void
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 设置自定义的userDetailsService
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
/**
* 密码加密
* @date: 2021/3/27 18:31
* @return: org.springframework.security.crypto.password.PasswordEncoder
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 配置请求地址的权限
http.authorizeRequests()
// 所有用户可访问,不需要登入
.antMatchers("/test/echo").permitAll()
// 设置前端页面
.antMatchers("/login.html").permitAll()
// 需要 ADMIN 权限
// .antMatchers("/test/admin","/test/getUserInfo").hasAuthority("ADMIN")
// 需要 ROLE_abc 角色,要去掉ROLE_前缀,不然会报错,SpringSecurity会自动拼接ROLE_前缀
.antMatchers("/test/admin","/test/getUserInfo").hasRole("abc")
// 需要 NORMAL 权限
.antMatchers("/test/normal","/test/getUserInfo").hasAuthority("NORMAL") // 需要 NORMAL 角色。
// 任何请求,访问的用户都需要经过认证
.anyRequest().authenticated();
//异常处理
http.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler);
// 设置 Form 表单登陆
http.formLogin()
//自定义登录页面
.loginPage("/showLogin")
//必须和表单提交的接口一样,会去执行自定义登录逻辑
.loginProcessingUrl("/login")
//登录成功后跳转页面,POST请求
.successForwardUrl("/toMain")
//登录失败后跳转页面,POST请求
.failureForwardUrl("/toError")
.permitAll();
// 禁止跨域
http.csrf().disable();
}
}
5. LoginController 页面跳转方法
@Controller
public class LoginController {
/**
* 页面跳转
* @return
*/
@GetMapping("/showLogin")
public String showLogin(){
return "login.html";
}
/**
* 成功后跳转页面
*/
@PostMapping("/toMain")
@PreAuthorize("hasRole('ROLE_abc')")
public String toMain() {
return "redirect:/main.html";
}
/**
* 失败后跳转页面
* @return
*/
@PostMapping("/toError")
public String toError() {
return "redirect:/error.html";
}
}
6. 异常处理信息
自定义 MyAccessDeniedHandler 实现 AccessDeniedHandler 代码如下
代码没有生效,也不知道什么原因???
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException e) throws IOException{
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().println(JSONUtil.parse(Result.forbidden(e.getMessage())));
response.getWriter().flush();
}
}
就改成全局捕获异常代码如下
/**
* 全局异常处理器
* @author admin
*
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理AccessDeineHandler无权限异常
* @param exception
* @return
*/
@ExceptionHandler(value = AccessDeniedException.class)
public Result exceptionHandler(AccessDeniedException exception){
log.error("不允许访问!原因是:",exception.getMessage());
return Result.forbidden(exception.getMessage());
}
}
三、前端页面配置
1. login.html 登入页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/login" method="post">
用户名:<input type="text" name="username"/><br/>
密码:<input type="password" name="password"/><br/>
记住我:<input type="checkbox" name="remember-me" value="true" hidden/><br/>
<input type="submit" value="登录"/>
</form>
</body>
</html>
2. main.html 主页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
登陆成功!!!
<a href="/logout">退出</a>
<a href="main1.html">跳转</a>
</body>
</html>
3. error.html 错误页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
操作失败,请重新登录 <a href= "/login.html">跳转</a>
</body>
</html>
四、验证效果
上面我们自定义了userDetailsService
,此时,Spring Security 在其作用流程中会调用自定义登录页面,会从新通过LoginController调用showLogin方法重定向login.html登入页面
我们输入用户名(admin )和密码(admin)点击登入跳转 main.html页面
当我在toMain方法中添加 @PreAuthorize("hasRole('ROLE_abc2')") 方法,因为admin方法没有ROLE_abc2
五、RememberMe功能实现
Spring Security 中 Remember Me 为“记住我”功能,用户只需要在登录时添加 remember-me复选框,取值为true。Spring Security 会自动把用户信息存储到数据源中,以后就可以不登录进行访问。
编写配置
/**
* 记住我配置类
* @author zhoubin
* @since 1.0.0
*/
@Configuration
public class RememberMeConfig {
@Autowired
private DataSource dataSource;
@Bean
public PersistentTokenRepository getPersistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new
JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//自动建表,第一次启动时需要,第二次启动时注释掉
// jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
}
修改SecurityConfig.java
http.rememberMe()
//失效时间,单位秒
.tokenValiditySeconds(600)
//登录逻辑交给哪个对象
.userDetailsService(userDetailsService)
// 持久层对象
.tokenRepository(persistentTokenRepository);
在客户端页面添加复选框
在客户端登录页面中添加 remember-me 的复选框,只要用户勾选了复选框下次就不需要进行登录了。
<form action="/login" method="post">
用户名:<input type="text" name="username" /><br/>
密 码:<input type="password" name="password" /><br/>
记住我:<input type="checkbox" name="remember-me" value="true"/><br/>
<input type="submit" value="登录" />
</form>
六、总结
本篇内容,我们通过一个小例子开始介绍了如何给web应用引入Spring Security保护;在展示了http-basic验证之后,我们使用了内存用户实验了“角色-资源”访问控制;然后我们介绍了spring security的一些核心概念;之后我们介绍了spring security 是通过filter的形式在web应用中发生作用的,并列举了filter列表,介绍了入口filter,介绍了springboot是如何载入spring security入口filter的。最后我们通过两个实战中的例子展示了spring security的使用。
Spring Security 功能也非常强大,但是还是挺复杂的,本篇内容如有差错还请指出,谢谢!
项目地址
security-example-03 这个模块