测试环境
springboot 2.1.7
security 5.1.6
jdk 1.8
mysql 8.0
Security介绍
Spring Security
是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring的IOC,DI,AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为安全控制编写大量重复代码的工作。
中文参考手册
https://springcloud.cc/spring-security.html
核心API
Spring Security的主要构建块
-
SecurityContextHolder
最基本的对象,保存着当前会话用户认证,权限,鉴权等核心数据。SecurityContextHolder
默认使用ThreadLocal
策略来存储认证信息,与线程绑定的策略。用户退出时,自动清除当前线程的认证信息
初始化源码
//能看到明显使用ThreadLocal线程就可以了
private static void initialize() {
if (!StringUtils.hasText(strategyName)) {
strategyName = "MODE_THREADLOCAL";
}
if (strategyName.equals("MODE_THREADLOCAL")) {
strategy = new ThreadLocalSecurityContextHolderStrategy();
} else if (strategyName.equals("MODE_INHERITABLETHREADLOCAL")) {
strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
} else if (strategyName.equals("MODE_GLOBAL")) {
strategy = new GlobalSecurityContextHolderStrategy();
} else {
try {
Class<?> clazz = Class.forName(strategyName);
Constructor<?> customStrategy = clazz.getConstructor();
strategy = (SecurityContextHolderStrategy)customStrategy.newInstance();
} catch (Exception var2) {
ReflectionUtils.handleReflectionException(var2);
}
}
++initializeCount;
}
-
Authentication
源代码
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
#是否被认证,认证为true
boolean isAuthenticated();
#设置是否能被认证
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
1)、getAuthorities,权限列表,通常是代表权限的字符串集合;
2)、getCredentials,密码,认证之后会移出,来保证安全性;
3)、getDetails,请求的细节参数;
4)、getPrincipal, 核心身份信息,一般返回UserDetails的实现类。
-
UserDetails
封装了用户的详细的信息。
源代码
public interface UserDetails extends Serializable {
#1.权限集合
Collection<? extends GrantedAuthority> getAuthorities();
#2.密码
String getPassword();
#3.用户民
String getUsername();
#4.用户是否过期
boolean isAccountNonExpired();
#5.是否锁定
boolean isAccountNonLocked();
#6.用户密码是否过期
boolean isCredentialsNonExpired();
#7.账号是否可用(可理解为是否删除)
boolean isEnabled();
}
-
UserDetailsService
用户需要实现该接口,自定义用户认证流程,通常读取数据库,对比用户的登录信息,完成认证,授权。
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
AuthenticationManager
认证流程顶级接口。该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数;可以通过实现AuthenticationManager
接口来自定义自己的认证方式,Spring提供了一个默认的实现,ProviderManager
。
public interface AuthenticationManager {
Authentication authenticate(Authentication var1) throws AuthenticationException;
}
总结
UserDetailsService
接口作为桥梁,是DaoAuthenticationProvier
与特定用户信息来源进行解耦的地方,UserDetailsService
由UserDetails
和UserDetailsManager
所构成;UserDetails
和UserDetailsManager
各司其责,一个是对基本用户信息进行封装,一个是对基本用户信息进行管理;
特别注意,UserDetailsService
、UserDetails
以及UserDetailsManager
都是可被用户自定义的扩展点,我们可以继承这些接口提供自己的读取用户来源和管理用户的方法,比如我们可以自己实现一个 与特定 ORM 框架,比如Mybatis
或者Hibernate
,相关的UserDetailsService
和UserDetailsManager
;
Demo例子
项目实现的流程描述
1)、三个页面分类,page1、page2、page3
2)、未登录授权都不可以访问
3)、登录后根据用户权限,访问指定页面
4)、对于未授权页面,访问返回403:资源不可用
5)、记住密码后,重启项目不需要登陆
项目中大部分都有注释,还有一些地方不知道为什么这么做的可以百度一下;
比如:security记住密码功能实现 相信度娘有很多的,可以告诉你
先看效果
数据库中的表是JPA自动生成的,不要自己建表哦 测试数据是添加的
当匿名用户(未登录状态)访问wg1会直接跳转到登陆界面,登陆root后因为它有访问wg1和wg3的权限 然后没有wg2的权限所以返回403 然后注销跳转到主页最开始的界面
记住密码演示看最后 就是重启项目6分钟内不用登陆了
总览项目结构图*
新建项目
核心依赖配置
Druid的配置参考:https://www.jianshu.com/p/c3006ac7e37c
server.port=8080
# ==============================
# MySQL connection config
# ==============================
spring.datasource.url=jdbc:mysql://localhost:3306/spring_demo?useUnicode=true&characeterEncoding=utf-8&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
#Druid会自动跟url识别驱动类名,如果连接的数据库非常见数据库,配置属性driverClassName
# ==============================
# Druid 数据源专用配置
# ==============================
# 初始化大小,最小,最大
spring.datasource.initialSize=3
spring.datasource.minIdle=5
spring.datasource.maxActive=20
# 配置获取连接等待超时的时间
spring.datasource.maxWait=30000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.datasource.timeBetweenEvictionRunsMillis=60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
spring.datasource.minEvictableIdleTimeMillis=300000
spring.datasource.maxEvictableIdleTimeMillis=900000
spring.datasource.validationQuery=SELECT 1 FROM DUAL
spring.datasource.testWhileIdle=true
spring.datasource.testOnBorrow=false
spring.datasource.testOnReturn=false
# 打开PSCache,并且指定每个连接上PSCache的大小
spring.datasource.poolPreparedStatements=true
spring.datasource.maxPoolPreparedStatementPerConnectionSize=20
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
spring.datasource.filters=stat,wall,log4j
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
# 合并多个DruidDataSource的监控数据
#spring.datasource.useGlobalDataSourceStat=true
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
spring.datasource.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=10000
# ==============================
# Thymeleaf configurations
# ==============================
spring.thymeleaf.mode=HTML
spring.thymeleaf.cache=false
spring.thymeleaf.servlet.content-type=text/html
spring.thymeleaf.encoding=UTF-8
# ==============================
# jpa configurations
# ==============================
#配置指明在程序启动的时候要删除并且创建实体类对应的表。这个参数很危险,
#因为他会把对应的表删除掉然后重建。所以千万不要在生成环境中使用。只有在测试环境中,一开始初始化数据库结构的时候才能使用一次。
#过后使用update
#spring.jpa.hibernate.ddl-auto=create
spring.jpa.hibernate.ddl-auto=update
# 配置在日志中打印出执行的 SQL 语句信息。
#spring.jpa.show-sql=true
#默认的存储引擎切换为 InnoDB
spring.jpa.database-platform=org.hibernate.dialect.MySQL57InnoDBDialect
启动类添加如下可以不启动security这里当然不是去设置不启动
@EnableAutoConfiguration(exclude = {
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class
})
security核心配置
SecurityConfig
package com.wg.securitydemo.config;
import com.wg.securitydemo.service.CustomPasswordEncoder;
import com.wg.securitydemo.service.UserDetailServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import javax.sql.DataSource;
/**
* EnableWebSecurity注解使得SpringMVC集成了Spring Security的web安全支持
*/
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 权限配置
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// super.configure(http);
// 禁用CSRF
http.csrf().disable();
// 配置拦截规则
http
.authorizeRequests()
.antMatchers("/").permitAll()//index.html页面不需要验证
.antMatchers("/page1/**").hasRole("LEVEL1")////数据库角色表的角色码必须加ROLE_开头,ROLE_LEVEL1
.antMatchers("/page2/**").hasRole("LEVEL2")
.antMatchers("/page3/**").hasRole("LEVEL3")
.anyRequest().authenticated();//任何尚未匹配的URL只需要对用户进行身份验证
// 配置登录功能
http
.formLogin().loginPage("/login")//更新的配置指定登录页面的位置
.usernameParameter("user")//取得表单中name为user的信息
.passwordParameter("pwd")//取得表单中name为pwd的信息
.successForwardUrl("/")
.failureForwardUrl("/403")
.permitAll();
// 注销成功跳转首页
http.logout().logoutUrl("/logout")//指定注销的页面url 你指定后 他就会自动注销了
.logoutSuccessUrl("/");
//开启记住我功能
http.rememberMe()
.rememberMeParameter("remeber")//取得表单中name为remeber的信息
.tokenRepository(persistentTokenRepository())//调用下面的记住密码的功能函数
.tokenValiditySeconds(360)// 失效时间以秒为单位
.userDetailsService(userDetailsService);
}
/**
* 自定义认证数据源
*/
@Autowired
UserDetailServiceImpl userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// super.configure(auth);
auth.userDetailsService(userDetailsService)//调用用户认证的类
.passwordEncoder(new CustomPasswordEncoder());//这里掉用的是自定义密码加密方式 好像密码加密方式必须要有 这里可以写个直接返回原密码的加密类
}
/**
*记住密码的功能
* */
@Autowired
private DataSource dataSource;
@Bean
public PersistentTokenRepository persistentTokenRepository () {
JdbcTokenRepositoryImpl tokenRepositoryImpl = new JdbcTokenRepositoryImpl();
tokenRepositoryImpl.setDataSource(dataSource);
// 启动时自动创建表 如果数据库有该表,再设置为true,启动会报错 所以第一次运行时开启 以后关闭
// tokenRepositoryImpl.setCreateTableOnStartup(true);
return tokenRepositoryImpl;
}
}
认证流程类UserDetailsService接口实现类
package com.wg.securitydemo.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
UserService userService;
@Autowired
User_RoleService user_roleService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 密码是根据username从数据库查询
String password = userService.GetpassByName(username);
// 角色权限是数据库权限表查询的,只保留权限那一字段的List<String>万一有多个呢
List<String> roleList = user_roleService.GetRoleByName(username);
List<GrantedAuthority> grantedAuthorityList = new ArrayList<>() ;
/*
* Spring Boot 2.0 版本踩坑
* 必须要 ROLE_ 前缀, 因为 hasRole("LEVEL1")判断时会自动加上ROLE_前缀变成 ROLE_LEVEL1 ,
* 如果不加前缀一般就会出现403错误
* 在给用户赋权限时,数据库存储必须是完整的权限标识ROLE_LEVEL1
*/
if (roleList != null && roleList.size()>0){
for (String role : roleList){
grantedAuthorityList.add(new SimpleGrantedAuthority(role)) ;
}
}
return new User(username,password,grantedAuthorityList);
}
}
对密码加密的处理(这里选择不处理)
package com.wg.securitydemo.service;
import org.springframework.security.crypto.password.PasswordEncoder;
//这里不对密码做任何处理简单Demo
public class CustomPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return s.equals(charSequence.toString());
}
}
页面控制类
package com.wg.securitydemo.controller;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class PageController {
@RequestMapping(value = "/")
public String Home(Model model){
//返回当前登录的用户信息,前面说了,他是存在SecurityContextHolder 的全局变量中,所以我们可以这样获取
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (!(auth instanceof AnonymousAuthenticationToken)) {//匿名用户 判断当前Authentication对象是否为一个AnonymousAuthenticationToken instanceof 严格来说是Java中的一个双目运算符,用来测试一个对象是否为一个类的实例
Object thisuser = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
model.addAttribute("my",thisuser);
}
return "index";
}
@RequestMapping(value = "/login")
public String Login(){
return "login";
}
@RequestMapping(value = "/logout")
public String Logout(){return "logout";}
@RequestMapping(value = "/403")
public String Error(){
return "403";
}
@RequestMapping(value = "page1/wg1")
public String GoToWg1(){
return "page1/wg1";
}
@RequestMapping(value = "page2/wg2")
public String GoToWg2(){
return "page2/wg2";
}
@RequestMapping(value = "page3/wg3")
public String GoToWg3(){
return "page3/wg3";
}
}
其余的代码我就直接按名字贴上来
- User
package com.wg.securitydemo.model;
import javax.persistence.*;
@Entity
@Table(name = "user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long uid;
private String username;
private String userpass;
public User() {
}
public Long getUid() {
return uid;
}
public void setUid(Long uid) {
this.uid = uid;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getUserpass() {
return userpass;
}
public void setUserpass(String userpass) {
this.userpass = userpass;
}
@Override
public String toString() {
return "User{" +
"uid=" + uid +
", username='" + username + '\'' +
", userpass='" + userpass + '\'' +
'}';
}
}
/********************************我是分割线***********************************/
- User_Role
package com.wg.securitydemo.model;
import javax.persistence.*;
@Entity
@Table(name = "user_role")
public class User_Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long rid;
private Long uid;
private String username;
private String userrole;
public User_Role() {
}
public Long getRid() {
return rid;
}
public void setRid(Long rid) {
this.rid = rid;
}
public Long getUid() {
return uid;
}
public void setUid(Long uid) {
this.uid = uid;
}
public String getUsername() {
return username;
}
public void setUser_name(String user_name) {
this.username = user_name;
}
public String getUser_role() {
return userrole;
}
public void setUser_role(String user_role) {
this.userrole = user_role;
}
@Override
public String toString() {
return "User_Role{" +
"rid=" + rid +
", uid=" + uid +
", user_name='" + username + '\'' +
", user_role='" + userrole + '\'' +
'}';
}
}
/********************************我是分割线***********************************/
- UserDao
package com.wg.securitydemo.dao;
import com.wg.securitydemo.model.User;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface UserDao extends CrudRepository<User,Long> {
List<User> findByUsername(String name);
}
/********************************我是分割线***********************************/
- User_RoleDao (记住请不要在数据库中用user_name这种名字 带有下划线的)
package com.wg.securitydemo.dao;
import com.wg.securitydemo.model.User_Role;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface User_RoleDao extends CrudRepository<User_Role,Long> {
List<User_Role> findByUsername(String name);
}
/********************************我是分割线***********************************/
- UserService
package com.wg.securitydemo.service;
import com.wg.securitydemo.dao.UserDao;
import com.wg.securitydemo.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
@Autowired
UserDao userDao;
public String GetpassByName(String name){
List<User> ulist = userDao.findByUsername(name);
if(ulist!=null&&ulist.size()>0)
return ulist.get(0).getUserpass();
else
return null;
}
}
/********************************我是分割线***********************************/
- User_RoleService
package com.wg.securitydemo.service;
import com.wg.securitydemo.dao.User_RoleDao;
import com.wg.securitydemo.model.User_Role;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class User_RoleService {
@Autowired
User_RoleDao user_roleDao;
public List<String> GetRoleByName(String name){
List<User_Role> rlist = user_roleDao.findByUsername(name);
List<String> slist = new ArrayList<>();
for(User_Role temp : rlist){
slist.add(temp.getUser_role());
}
return slist;
}
}
/********************************我是分割线***********************************/
- index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页登陆</title>
</head>
<body>
<p th:text="${my}"></p>
<p th:if="${my!=null}" style="text-align: center"><a href="/logout">注销</a></p>
<h3 th:if="${my==null}" style="text-align: center">欢迎来到首页<a href="/login">请登录</a></h3>
<h3 style="text-align: center"><a href="/page1/wg1">去wg1</a> <a href="/page2/wg2">去wg2</a> <a href="/page3/wg3">去wg3</a></h3>
</body>
</html>
/********************************我是分割线***********************************/
- login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登陆</title>
</head>
<body>
<div align="center">
<form th:action="@{/login}" method="post">
用户名:<input name="user"/><br>
密 码:<input name="pwd"><br/>
<input type="checkbox" name="remeber"> 记住我<br/>
<input type="submit" value="Login">
</form>
</div>
</body>
</html>
/********************************我是分割线***********************************/
- logout.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>LogOUT</title>
</head>
<body>
<p style="text-align: center">注销成功</p>
</body>
</html>
/********************************我是分割线***********************************/
- 403.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>403</title>
</head>
<body>
<h1 style="text-align: center">403</h1>
</body>
</html>
/********************************我是分割线***********************************/
wg1.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WG</title>
</head>
<body>
<P style="text-align: center">欢迎来到page1/wg1</P>
</body>
</html>
/********************************我是分割线***********************************/
- wg2.html wg3.html -.-eMMMMMMM。。。你懂的
- 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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.wg</groupId>
<artifactId>securitydemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>securitydemo</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-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<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>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.9</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</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>
</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>
运行和记住密码
第一次会自动生成数据库中表
下面这个表用来记住用户的
验证记住密码重启时