文章目录
一。Spring Security介绍
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。由于它 是Spring生态系统中的一员,因此它伴随着整个Spring生态系统不断修正、升级,在spring boot项目中加入spring security更是十分简单,使用Spring Security 减少了为企业系统安全控制编写大量重复代码的工作。
二。工程搭建
1.工程结构图:
2.导入依赖
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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.itheima.security</groupId>
<artifactId>security-springboot</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<!-- 以下是>spring boot依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 以下是>spring security依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 以下是jsp依赖-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<!--jsp页面使用jstl标签 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<!--用于编译jsp -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
</dependencies>
<build>
<finalName>security-springboot</finalName>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<encoding>utf-8</encoding>
<useDefaultDelimiters>true</useDefaultDelimiters>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>**/*</include>
</includes>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
其中,整合spring security最主要的依赖如下所示:
<!-- 以下是>spring security依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
3.编写security配置类
新建WebConfig配置
类,实现WebMvcConfigurer
接口,配置项目启动默认跳转页面,这里是配置到login.jsp页面:
package com.security.springboot.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration//就相当于springmvc.xml文件
public class WebConfig implements WebMvcConfigurer {
//默认Url根路径跳转到/login,此url为spring security提供
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("redirect:/login-view");
registry.addViewController("/login-view").setViewName("login");
}
}
新建WebSecurityConfig
配置类,实现WebSecurityConfigurerAdapter
接口,配置安全拦截机制:
package com.security.springboot.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;
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//密码编码器(用户密码存入数据为BCryptPasswordEncoder加密的方式存入)
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//安全拦截机制(最重要)
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
.anyRequest().permitAll()//除了/r/**,其它的请求可以访问
.and()
.formLogin()//允许表单登录
.loginPage("/login-view")//登录页面
.loginProcessingUrl("/login")
.successForwardUrl("/login-success")//自定义登录成功的页面地址
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login-view?logout");
}
}
4.编写UserDetailsService验证类
什么是UserDetailsService ?
现在咱们现在知道DaoAuthenticationProvider处理了web表单的认证逻辑,认证成功后既得到一个 Authentication(UsernamePasswordAuthenticationToken实现),里面包含了身份信息(Principal)。这个身份 信息就是一个 Object ,大多数情况下它可以被强转为UserDetails对象。
DaoAuthenticationProvider中包含了一个UserDetailsService实例,它负责根据用户名提取用户信息 UserDetails(包含密码),而后DaoAuthenticationProvider会去对比UserDetailsService提取的用户密码与用户提交 的密码是否匹配作为认证成功的关键依据,因此可以通过将自定义的 UserDetailsService 公开为spring bean来定 义自定义身份验证。
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
很多人把DaoAuthenticationProvider和UserDetailsService的职责搞混淆,其实UserDetailsService只负责从特定 的地方(通常是数据库)加载用户信息,仅此而已。而DaoAuthenticationProvider的职责更大,它完成完整的认 证流程,同时会把UserDetails填充至Authentication。
上面一直提到UserDetails是用户信息,咱们看一下它的真面目:
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
它和Authentication接口很类似,比如它们都拥有username,authorities。Authentication的getCredentials()与 UserDetails中的getPassword()需要被区分对待,前者是用户提交的密码凭证,后者是用户实际存储的密码,认证 其实就是对这两者的比对。Authentication中的getAuthorities()实际是由UserDetails的getAuthorities()传递而形 成的。还记得Authentication接口中的getDetails()方法吗?其中的UserDetails用户详细信息便是经过了 AuthenticationProvider认证之后被填充的。
通过实现UserDetailsService和UserDetails,我们可以完成对用户信息获取方式以及用户信息字段的扩展。 Spring Security提供的InMemoryUserDetailsManager(内存认证),JdbcUserDetailsManager(jdbc认证)就是 UserDetailsService的实现类,主要区别无非就是从内存还是从数据库加载用户。
编写SpringDataUserDetailsService
类,实现UserDetailsService
接口,从数据库查询用户信息。(认证逻辑会首先通过这个验证类来验证用户是否存在)
package com.security.springboot.service;
import com.security.springboot.dao.UserDao;
import com.security.springboot.model.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;
@Service
public class SpringDataUserDetailsService implements UserDetailsService {
@Autowired
UserDao userDao;
//根据 账号查询用户信息
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//将来连接数据库根据账号查询用户信息
User user = userDao.getUserByUsername(username);
if(user == null){
//如果用户查不到,返回null,由provider来抛出异常
return null;
}
//根据用户的id查询用户的权限
List<String> permissions = userDao.findPermissionsByUserId(user.getId());
//将permissions转成数组
String[] permissionArray = new String[permissions.size()];
permissions.toArray(permissionArray);
UserDetails userDetails = org.springframework.security.core.userdetails.User.withUsername(user.getUsername()).password(user.getPassword()).authorities(permissionArray).build();
return userDetails;
}
}
5.Dao层
编写UserDao类,从数据库获取信息:
package com.security.springboot.dao;
import com.security.springboot.model.Permission;
import com.security.springboot.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.List;
@Repository
public class UserDao {
@Autowired
JdbcTemplate jdbcTemplate;
//根据账号查询用户信息
public User getUserByUsername(String username){
String sql = "select id,username,password,fullname,mobile from t_user where username = ?";
//连接数据库查询用户
List<User> list = jdbcTemplate.query(sql, new Object[]{
username}, new BeanPropertyRowMapper<>(User.class));
if(list !=null && list.size()==1){
return list.get(0);
}
return null;
}
//根据用户id查询用户权限
public List<String> findPermissionsByUserId(String userId){
String sql = "SELECT * FROM t_permission WHERE id IN(\n" +
"\n" +
"SELECT permission_id FROM t_role_permission WHERE role_id IN(\n" +
" SELECT role_id FROM t_user_role WHERE user_id = ? \n" +
")\n" +
")\n";
List<Permission> list = jdbcTemplate.query(sql, new Object[]{
userId}, new BeanPropertyRowMapper<>(Permission.class));
List<String> permissions = new ArrayList<>();
list.forEach(c -> permissions.add(c.getCode()));
return permissions;
}
}
6.model层
User:
package com.security.springboot.model;
import lombok.Data;
/**
* 用户实体
*/
@Data
public class User {
private String id;
private String username;
private String password;
private String fullname;
private String mobile;
}
Permission:
package com.security.springboot.model;
import lombok.Data;
/**
* 用户关联权限实体
*/
@Data
public class Permission {
private String id;
private String code; //用户权限
private String description; //权限描述
private String url; //资源url
}
7.控制器层
LoginController:
package com.security.springboot.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class LoginController {
/**
* 登录成功执行接口
* @return
*/
@RequestMapping(value = "/login-success",produces = {
"text/plain;charset=UTF-8"})
public String loginSuccess(){
//提示具体用户名称登录成功
return getUsername()+" 登录成功";
}
/**
* 测试资源1
* @return
*/
@GetMapping(value = "/r/r1",produces = {
"text/plain;charset=UTF-8"})
@PreAuthorize("hasAuthority('p1')")//拥有p1权限才可以访问
public String r1(){
return getUsername()+" 访问资源1";
}
/**
* 测试资源2
* @return
*/
@GetMapping(value = "/r/r2",produces = {
"text/plain;charset=UTF-8"})
@PreAuthorize("hasAuthority('p2')")//拥有p2权限才可以访问
public String r2(){
return getUsername()+" 访问资源2";
}
/**
* 无需认证测试接口
* @return
*/
@GetMapping(value = "test",produces = {
"text/plain;charset=UTF-8"})
public String test(){
return "无需认证测试接口 ";
}
//获取当前用户信息
private String getUsername(){
String username = null;
//当前认证通过的用户身份
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//用户身份
Object principal = authentication.getPrincipal();
if(principal == null){
username = "匿名";
}
if(principal instanceof org.springframework.security.core.userdetails.UserDetails){
UserDetails userDetails = (UserDetails) principal;
username = userDetails.getUsername();
}else{
username = principal.toString();
}
return username;
}
}
8.工具类
因为SpringSecurity
框架中,数据库存放的用户密码是以加密的方式来进行存储的,在本项目我们采用BCryptPasswordEncoder
加密。
首先我们需要在之前的WebSecurityConfig
配置类中声明:
BCryptPasswordEncoder
加密工具类BCPEUtils
代码如下所示:
package com.security.springboot.util;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/**
* SpringSecurity中的BCryptPasswordEncoder加密
*/
public class BCPEUtils {
private static BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
/**
* 将原生密码进行加密
* @return 加密后的字符串密码
*/
public static String encode(String rawPWD){
String hashedPassword = passwordEncoder.encode(rawPWD);
return hashedPassword;
}
/**
* 比较原生密码与密文是否一致
* @param rawPWD 原生密码
* @param encodePWD 密文
* @return true/false
*/
public static boolean matches(String rawPWD,String encodePWD){
boolean result = passwordEncoder.matches(rawPWD, encodePWD);
return result;
}
public static void main(String[] args) {
String password="123456";
String rePassword=BCPEUtils.encode(password);
System.out.println(rePassword);
System.out.println("----------------------");
System.out.println(BCPEUtils.matches(password,rePassword));
}
}
9.配置文件
application.yml配置内容如下:
server:
port: 8080
servlet:
context-path: /security-springboot #访问前缀
spring:
application:
name: security-springboot
mvc:
view: #视图解析器
prefix: /WEB-INF/view/
suffix: .jsp
datasource: #连接数据库信息
url: jdbc:mysql://localhost:3306/user_db
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
10.jsp示例登录页面
login.jsp
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="utf-8" %>
<html>
<head>
<title>用户登录</title>
</head>
<body>
<form action="login" method="post">
用户名:<input type="text" name="username"><br>
密 码:
<input type="password" name="password"><br>
<input type="submit" value="登录">
</form>
</body>
</html>
11.启动类
SecuritySpringBootApp:
package com.security.springboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SecuritySpringBootApp {
public static void main(String[] args) {
SpringApplication.run(SecuritySpringBootApp.class,args);
}
}
三。数据库结构
由于本项目中的验证是从数据库中读取用户数据,所以我们需要创建一个名为user_db
的数据库,并创建五张数据表
t_user(用户表)
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` bigint(20) NOT NULL 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 NOT NULL COMMENT '用户姓名',
`mobile` varchar(11) 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;
INSERT INTO `t_user` VALUES (1, 'zhangsan', '$2a$10$3MYHLGvYkYgCKRAXNeEDTe1g7AznV/Ni7pmXP2zD0V0YS/HosBamu', '张三', '12345674891');
创建完成后如下所示:
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 NULL DEFAULT NULL,
`update_time` datetime 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;
INSERT INTO `t_role` VALUES ('1', '管理员', NULL, NULL, NULL, '');
创建完成后如下所示:
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;
INSERT INTO `t_permission` VALUES ('1', 'p1', '测试资源 1', '/r/r1');
INSERT INTO `t_permission` VALUES ('2', 'p3', '测试资源2', '/r/r2');
创建完成后如下所示:
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 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;
INSERT INTO `t_user_role` VALUES ('1', '1', NULL, NULL);
创建完成后如下所示:
t_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;
INSERT INTO `t_role_permission` VALUES ('1', '1');
INSERT INTO `t_role_permission` VALUES ('1', '2');
创建完成后如下所示:
四。测试
1.用户登录验证
1.Debug启动项目,在SpringDataUserDetailsService
上面打断点:
2.在浏览器访问 http://localhost:8080/security-springboot,自动跳到登录页面:
3.输入用户名以及用户密码,点击登录按钮:
4.跳到后台SpringDataUserDetailsService
用户验证逻辑中,拿到用户信息:
5.用户验证成功后:
2.用户权限验证
1.访问http://localhost:8080/security-springboot/r/r1,由于张三角色含有p1,p3权限,所以可以成功进行访问:
2.访问http://localhost:8080/security-springboot/r/r2,访问失败:
3.无需权限认证访问接口,访问成功:
五。项目地址
该项目已经上传至百度网盘,有需要的小伙伴可自行下载:
链接:https://pan.baidu.com/s/1g6EqQMvje653zTjySy-sCA
提取码:lig3