技术点:
- Spring Boot
- Spring Security
- MySQL
- MyBatis
- Thymeleaf
本项目GitHub传送门
一、项目框架
1. 项目结构
2. 选择依赖
使用Spring Boot 创建项目。
依赖勾选
- Spring Boot DevTools
- Spring Web
- Spring Security
- Thymeleaf
- Mybatis Framework
- MySQL Driver
可参考此pom.xml
文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.aaa</groupId>
<artifactId>springboot-security-thymeleaf</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-security-thymeleaf</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</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.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
二、数据库
共5张表
- t_user 用户表
- t_role 角色表
- r_user_role 用户-角色
- r_permission 权限表
- r_role_permission 角色-权限
/*
Navicat Premium Data Transfer
Source Server : localhost
Source Server Type : MySQL
Source Server Version : 50610
Source Host : localhost:3306
Source Schema : user_db
Target Server Type : MySQL
Target Server Version : 50610
File Encoding : 65001
Date: 21/07/2020 21:29:29
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_permission
-- ----------------------------
DROP TABLE IF EXISTS `t_permission`;
CREATE TABLE `t_permission` (
`id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`code` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '权限标识符',
`description` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '描述',
`url` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '请求地址',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of t_permission
-- ----------------------------
INSERT INTO `t_permission` VALUES ('1', 'p1', '测试资源1', '/resource/r1');
INSERT INTO `t_permission` VALUES ('2', 'p2', '测试资源2', '/resource/r2');
INSERT INTO `t_permission` VALUES ('3', 'p3', '测试资源3', '/resource/r3');
-- ----------------------------
-- Table structure for t_role
-- ----------------------------
DROP TABLE IF EXISTS `t_role`;
CREATE TABLE `t_role` (
`id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`role_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`description` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`create_time` datetime(0) NULL DEFAULT NULL,
`update_time` datetime(0) NULL DEFAULT NULL,
`status` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `unique_role_name`(`role_name`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of t_role
-- ----------------------------
INSERT INTO `t_role` VALUES ('1', '管理员', NULL, NULL, NULL, '');
-- ----------------------------
-- Table structure for t_role_permission
-- ----------------------------
DROP TABLE IF EXISTS `t_role_permission`;
CREATE TABLE `t_role_permission` (
`role_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`permission_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
PRIMARY KEY (`role_id`, `permission_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of t_role_permission
-- ----------------------------
INSERT INTO `t_role_permission` VALUES ('1', '1');
INSERT INTO `t_role_permission` VALUES ('1', '2');
-- ----------------------------
-- Table structure for t_user
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户id',
`username` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`password` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`fullname` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户姓名',
`mobile` varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '手机号',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of t_user
-- ----------------------------
INSERT INTO `t_user` VALUES (1, 'zhangsan', '$2a$10$37vdSYJUVguwXpLDnZfEt.UDC0y6Yk2RCzFuJKfOrWCiTnUFlmj3K', NULL, NULL);
-- ----------------------------
-- Table structure for t_user_role
-- ----------------------------
DROP TABLE IF EXISTS `t_user_role`;
CREATE TABLE `t_user_role` (
`user_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`role_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`create_time` datetime(0) NULL DEFAULT NULL,
`creator` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`user_id`, `role_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of t_user_role
-- ----------------------------
INSERT INTO `t_user_role` VALUES ('1', '1', NULL, NULL);
SET FOREIGN_KEY_CHECKS = 1;
三、代码实现
实现思路
- 配置Spring Boot环境。将服务器配置、数据源、MyBatis映射文件、启动文件完成配置。
- 完成基本的html(Thymeleaf)页面。用于测试用户注册、登录、访问资源功能。
- 配置html视图器。将使用Thymeleaf中的视图进行配置。
- 进行Spring Security配置。主要配置密码编码器、安全拦截器、登录页面。
- 编写dao层和mapper文件。用于连接数据库。
- 编写service层。一方面是实现用户的注册和登录,另一方面是实现spring-security核心接口,其负载着用户特定数据,它被用来在整个框架作为一个用户DAO。
- 编写controller层。用于页面访问,在controller接口中可以设置接口的访问权限,让Spring Security进行安全验证。
1. 配置文件
application.properties
# web
server.port=8080
server.servlet.context-path=/
spring.application.name=security-springboot
# DataSource
spring.datasource.url=jdbc:mysql://localhost:3306/user_db?serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# mybatis
mybatis.mapper-locations=classpath:mapper/*.xml
2. 编写测试页面
index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org/">
<head>
<meta charset="UTF-8">
<title>index</title>
</head>
<body>
登录成功
<h1>菜单页</h1>
<hr>
<h3><a href="/resource/r1" target="_blank">资源一</a></h3>
<h3><a href="/resource/r2" target="_blank">资源二</a></h3>
<h3><a href="/resource/r3" target="_blank">资源三</a></h3>
<hr>
<form th:action="@{/logout}" method="get" >
<button type="submit" >退出</button>
</form>
</body>
</html>
login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org/">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<form th:action="@{/loginForm}" method="post" >
用户名<input type="text" name="username" /><br>
密码<input type="password" name="password" /><br>
<button type="submit" >提交</button>
<!-- ${session?.SPRING_SECURITY_LAST_EXCEPTION?.message} security自带的错误提示信息 -->
<p th:if="${param.error}" th:text="${session?.SPRING_SECURITY_LAST_EXCEPTION?.message}" ></p>
</form>
<a th:href="@{/register}">注册</a>
</body>
</html>
register.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>注册</title>
</head>
<body>
<h1>注册</h1>
<form action="/user/register" method="post">
用户名:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
<input type="submit" value="注册">
</form>
<hr>
<p><a href="/">登录</a></p>
</body>
</html>
3. 配置视图访问器
htmlController
package com.aaa.springbootsecuritythymeleaf.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
/**
* @author 淮南King
* @date 2020-08-04
*/
@RestController
public class htmlController {
@RequestMapping("/index")
public ModelAndView index(){
return new ModelAndView("/index");
}
@RequestMapping("/test")
public Object test(){
return "test 此请求无需权限";
}
/**
* 自定义登录页面
* @param error 错误信息显示标识
* @return
*
*/
@GetMapping("/login")
public ModelAndView login(String error){
ModelAndView modelAndView = new ModelAndView("/login");
modelAndView.addObject("error", error);
return modelAndView;
}
/**
* 自定义注册页面
* @return
*
*/
@RequestMapping("/register")
public ModelAndView register(){
return new ModelAndView("register");
}
}
4. 进行Spring Security配置
WebSecurityConfig
package com.aaa.springbootsecuritythymeleaf.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* spring security配置
*
* @author 淮南King
*/
@Configuration @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig
extends WebSecurityConfigurerAdapter {
//密码编码器
@Bean public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//安全拦截机制
@Override protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
//允许表单登录
.formLogin()
//登录页面路径
.loginPage("/login")
.loginProcessingUrl("/loginForm")
//设置登录成功跳转页面,error=true控制页面错误信息的展示
.successForwardUrl("/index").failureUrl("/login?error=true")
.permitAll()
.and()
//允许不登陆就可以访问的方法,多个用逗号分隔
.authorizeRequests().antMatchers("/test","/user/**","/register").permitAll()
//其他的需要授权后访问
.anyRequest().authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
.logout()
//登录退出
.logoutUrl("/logout")
//退出时情况cookies
.and()
.logout().deleteCookies("JESSIONID")
.logoutSuccessUrl("/login?logout");
}
}
MyUserDetail
package com.aaa.springbootsecuritythymeleaf.config;
import com.aaa.springbootsecuritythymeleaf.dao.UserDao;
import com.aaa.springbootsecuritythymeleaf.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 实现spring-security核心接口,其负载的用户特定数据。
*
* @author 淮南King
*/
@Service
public class MyUserDetail implements UserDetailsService {
@Autowired UserDao userDao;
// 根据账号查询用户信息
@Override public UserDetails loadUserByUsername(String username) {
//将来连接数据库根据账号查询用户信息
User userDto = userDao.getUserByUsername(username);
//当查询此用户不存在时,将抛出用户名未找到异常
if (userDto == null) {
throw new UsernameNotFoundException("No such user found, the user name is: "+username);
}
//根据用户id查询权限
List<String> permissions = userDao.findPermissionsByUserId(userDto.getId());
//将permissions转为数组
String[] permissionArray = new String[permissions.size()];
permissions.toArray(permissionArray);
UserDetails userDetails =
org.springframework.security.core.userdetails.User.withUsername(userDto.getUsername()).password(userDto.getPassword()).authorities(permissionArray)
.build();
return userDetails;
}
}
5. 编写dao层和mapper文件
UserDao.java
package com.aaa.springbootsecuritythymeleaf.dao;
import com.aaa.springbootsecuritythymeleaf.entity.User;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* 用户信息持久层
* @author 淮南King
*/
@Mapper
public interface UserDao {
/**
* 根据账号查询用户信息
*
* @param username 用户姓名
* @return 用户信息
*/
User getUserByUsername(String username);
/**
* 根据用户id查询用户权限
*
* @param userId 用户id
* @return 权限列表
*/
List<String> findPermissionsByUserId(String userId);
/**
* 添加用户
*
* @param userDTO 用户信息
* @return 修改条数
*/
int addUser(User userDTO);
}
UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.aaa.springbootsecuritythymeleaf.dao.UserDao">
<select id="getUserByUsername" parameterType="String" resultType="com.aaa.springbootsecuritythymeleaf.entity.User">
select id,username,password,fullname,mobile from t_user where username = #{username}
</select>
<select id="findPermissionsByUserId" parameterType="String" resultType="String">
SELECT code FROM t_permission WHERE id IN(
SELECT permission_id FROM t_role_permission WHERE role_id IN(
SELECT role_id FROM t_user_role WHERE user_id = #{id} ))
</select>
<insert id="addUser" parameterType="com.aaa.springbootsecuritythymeleaf.entity.User">
INSERT INTO `user_db`.`t_user`(`username`, `password`) VALUES (#{username},#{password})
</insert>
</mapper>
6. 编写service层
UserService
package com.aaa.springbootsecuritythymeleaf.service;
import com.aaa.springbootsecuritythymeleaf.dao.UserDao;
import com.aaa.springbootsecuritythymeleaf.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 用户信息持久业务层
*
* @author 淮南King
* @date 2020-07-21
*/
@Service
public class UserService {
@Autowired UserDao dao;
public User getUserByUsername(String username) {
return dao.getUserByUsername(username);
}
public List<String> findPermissionsByUserId(String userId) {
return dao.findPermissionsByUserId(userId);
}
/**
* 添加用户
* @param user
* @return
*/
public int addUser(User user) {
//获取密码编码器
PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
//将用户的密码进行编码
String password = passwordEncoder.encode(user.getPassword());
//将编码后的密码覆盖到用户信息中
user.setPassword(password.substring(8));
//将用户信息持久化到数据库中
return dao.addUser(user);
}
}
7. 编写controller层
LoginController
package com.aaa.springbootsecuritythymeleaf.controller;
import com.aaa.springbootsecuritythymeleaf.entity.User;
import com.aaa.springbootsecuritythymeleaf.service.UserService;
import com.aaa.springbootsecuritythymeleaf.util.SecurityUtil;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
/**
* @author 淮南King
*/
@RestController
public class LoginController {
@Resource private UserService service;
@RequestMapping(value = "/login-success",produces = {"text/plain;charset=UTF-8"})
public ModelAndView loginSuccess(){
//获取当前线程的SecurityContext
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//获取当前线程中用户的名称,将名称传递至页面
Map<String,Object> attributes = new HashMap<>();
attributes.put("username", SecurityUtil.getUserNameByAuthentication(authentication));
return new ModelAndView("menu",attributes);
}
@PostMapping("/user/register")
public String register(User user) {
service.addUser(user);
return user.getUsername()+"注册成功";
}
@GetMapping("/register")
public ModelAndView registerView() {
return new ModelAndView("register");
}
}
AuthController
package com.aaa.springbootsecuritythymeleaf.controller;
import com.aaa.springbootsecuritythymeleaf.util.SecurityUtil;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author 淮南King
* @date 2020-07-21
*/
@RestController
@RequestMapping("/resource")
public class AuthController {
/**
* 测试资源1
* 拥有p1权限才可以访问
*
* @return
*/
@GetMapping(value = "/r1", produces = {"text/plain;charset=UTF-8"})
@PreAuthorize("hasAuthority('p1')")
public String resource1() {
//获取当前线程的SecurityContext
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//获取当前线程的名称
return SecurityUtil.getUserNameByAuthentication(authentication) + " 访问资源1";
}
/**
* 测试资源2
* 拥有p2权限才可以访问
*
* @return
*/
@GetMapping(value = "/r2", produces = {"text/plain;charset=UTF-8"})
@PreAuthorize("hasAuthority('p2')")
public String resource2() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return SecurityUtil.getUserNameByAuthentication(authentication) + " 访问资源2";
}
/**
* 测试资源3
* 拥有p3权限才可以访问
*
* @return
*/
@GetMapping(value = "/r3", produces = {"text/plain;charset=UTF-8"})
@PreAuthorize("hasAuthority('p3')")
public String resource3() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return SecurityUtil.getUserNameByAuthentication(authentication) + " 访问资源3";
}
}
附:实体类与帮助类
User
package com.aaa.springbootsecuritythymeleaf.entity;
/**
* DTO:与数据库保持一致<br>
* 用户信息
* @author 淮南King
*/
public class User {
/**
* 用户id
*/
private String id;
/**
* 用户名
*/
private String username;
/**
* 用户密码
*/
private String password;
/**
* 用户角色ID
*/
private Integer roleId;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Integer getRoleId() {
return roleId;
}
public void setRoleId(Integer roleId) {
this.roleId = roleId;
}
}
Permission
package com.aaa.springbootsecuritythymeleaf.entity;
/**
* 权限信息
* @author 淮南King
*/
public class Permission {
/**
* 权限id
*/
private String id;
/**
* 权限代号
*/
private String code;
/**
* 权限描述
*/
private String description;
/**
* 路径
*/
private String url;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
}
SecurityUtil
package com.aaa.springbootsecuritythymeleaf.util;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
/**
* Security帮助类
* @author 淮南King
* @date 2020-07-21
*/
public class SecurityUtil {
/**
* 根据当前执行线程的SecurityContext获取用户名称
* @param authentication 当前认证通过的用户身份
* @return 用户名
*/
public static String getUserNameByAuthentication(Authentication authentication){
String username = null;
//用户身份
Object principal = authentication.getPrincipal();
if(principal == null){
return "匿名";
}
if(principal instanceof UserDetails){
UserDetails userDetails = (UserDetails)principal;
username = userDetails.getUsername();
}else{
username = principal.toString();
}
return username;
}
}
end…