Spring Security 上

Security-dome

1.创建项目

创建一个Spring Boot项目,不用加入什么依赖

2.导入依赖

<dependencies>
<!--启动器变为 web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--security启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

3.创建控制层

@RestController
public class TestController {
@GetMapping("/hello")
public String hello(){
return "hello Security";
}
}

4.配置文件修改端口号

server.port=8081

5.运行测试

运行网址为:

http://localhost:8081/hello

这时候会发现,网址会自动变为:

http://localhost:8081/login

6.登录

能看到,在该页面中有账号密码

默认账号:user

默认密码:

登录之后:

Security 原理

Spring Security 本质是一个过滤器链

FilterSecurityInterceptor:是一个方法级的 权限过滤器 ,基本位于过滤链的最底部


ExceptionTranslationFilter:是个异常过滤器,用来处理在认证授权过程中抛出的异常


UsernamePasswordAuthenticationFilter:对 /login 的POST请求做拦截,校验表单中用户名,密码


过滤器加载步骤

步骤流程

使用Spring Security配置过滤器 : DelegatingFilterProxy

源代码如下

public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
Filter delegateToUse = this.delegate;
if (delegateToUse == null) {
synchronized(this.delegateMonitor) {
delegateToUse = this.delegate;
if (delegateToUse == null) {
WebApplicationContext wac = this.findWebApplicationContext();
if (wac == null) {
throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener or DispatcherServlet registered?");
} delegateToUse = this.initDelegate(wac); }
this.delegate = delegateToUse;
}
}
this.invokeDelegate(delegateToUse, request, response, filterChain);
}

即为:

public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//进行判断 //初始化
delegateToUse = this.initDelegate(wac); //其余部分
}

然后我们查看 initDelegate

初始化为 FilterChainProxy 对象

进入 FilterChainProxy:

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
if (!clearContext) {
//满足条件 运行该方法
this.doFilterInternal(request, response, chain); } else {
try {
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
//不满足 最终还是需要运行该方法
this.doFilterInternal(request, response, chain); } catch (RequestRejectedException var9) {
this.requestRejectedHandler.handle((HttpServletRequest)request, (HttpServletResponse)response, var9);
} finally {
SecurityContextHolder.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
}
}

可以看出,无论满不满足条件,最终都需要运行 doFilterInternal()方法

private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
/*
* 部分代码。。。
*/
List<Filter> filters = this.getFilters((HttpServletRequest)firewallRequest);
/*
* 部分代码。。。
*/
    private List<Filter> getFilters(HttpServletRequest request) {
int count = 0;
Iterator var3 = this.filterChains.iterator();
SecurityFilterChain chain;
do {
if (!var3.hasNext()) {
return null;
}
chain = (SecurityFilterChain)var3.next();
if (logger.isTraceEnabled()) {
++count;
logger.trace(LogMessage.format("Trying to match request against %s (%d/%d)", chain, count, this.filterChains.size()));
}
} while(!chain.matches(request));
//返回所有过滤器
return chain.getFilters();
}

所以 doFilterInternal() 方法 可以返回 所有要进行加载的过滤器


总结:

  1. 配置过滤器 DelegatingFilterProxy
  2. 在其中进行初始化 initDelegate
  3. 在初始化中得到 FilterChainProxy 对象
  4. 在其中运行的就是 doFilterInternal() 方法,该方法返回的就是 所有要进行加载的过滤器

UserDetailsService 接口

UserDetailsService接口 : 查询数据库用户名和密码过程

步骤:

  1. 创建类继承UsernamePasswordAuthenticationFilter,重写三个方法: attemptAuthentication() 、successfulAuthentication()、unsuccessfulAuthentication()
  2. 如果成功调用successfulAuthentication(),反之调用unsuccessfulAuthentication()
  3. 创建类实现UserDetailService,编写查询数据过程,返回User对象,这个User对象是安全框架提供对象

PasswordEncoder接口

PasswordEncoder接口 : 数据加密接口,用于返回User对象里面密码加密

加密方法:

BCryptPasswordEncoder是Spring Security官方推荐的密码解析器,平时多使用这个解析器。

BCryptPasswordEncoder是对bcrypt强散列方法的具体实现。是基于Hash算法实现的单向加密。可以通过strength控制加密强度,默认10.

BCryptPasswordEncoder b = new BCryptPasswordEncoder();
String zc = b.encode("zc"); //加密成功

Web权限

Security-dome 中可以看到,如果想要进入页面,还需要输入账号密码

而对于登陆时候的账号密码可以进行自定义设置

  1. 通过配置文件
  2. 通过配置类
  3. 自定义编写实现类

1.通过配置文件

spring.security.user.name=root
spring.security.user.password=root

这个时候再运行,会发现控制台不会出现密码,可以直接通过设置的账号密码登录

2.通过配置类

  1. 创建一个 SecurityConfig 配置类
  2. 重写configure()方法,注意看清参数,不要选错方法
  3. 很重要的一点:需要注入PasswordEncoder接口

如果不注入该接口,可能报 Encoded password does not look like BCrypt

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String password = bCryptPasswordEncoder.encode("root");
auth.inMemoryAuthentication()
.withUser("root") //账号
.password(password) //加密的密码
.roles("admin"); //权限
} @Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}

这时候,也可以直接使用你设置的账号密码登录页面

3.自定义编写实现类

  1. 编写userDetailsService实现类,返回User对象
  2. 创建一个 SecurityConfig 配置类

编写一个UserDetailsService实现类

在其中需要重写 loadUserByUsername() 方法,该方法用于登录

@Service("userDetailsService")
public class MyuserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
List<GrantedAuthority> auths =
AuthorityUtils.commaSeparatedStringToAuthorityList("role");
//返回的实际上是一个User对象,参数解析可以看下面
return new User("root",
new BCryptPasswordEncoder().encode("root"),auths);
}
}

UserDetailsService 解析

对于该实现类中重写的 loadUserByUsername() 方法,返回的是 UserDetails 接口

在源代码中可以看出,实际上 UserDetails 接口,返回的是一个 User 对象

而在User对象中,需要返回三个参数:

String、String、Collection;

账号 、 密码 、集合(权限等信息)


创建一个 SecurityConfig 配置类

@Configuration
public class SecurityConfigTest extends WebSecurityConfigurerAdapter { @Autowired
UserDetailsService userDetailsService; // 这里应和 @Service("userDetailsService") 中内容相同 @Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//使用该方法
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
} @Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}

这时候测试,也可以直接使用设置的账号密码登录

之后,如果连接数据库,一般都是用第三种方式

4.连接数据库完成用户认证

(该方法是在第三种方法代码基础上完成)

  1. 创建数据库
  2. 整合Mybatis-Plus完成数据库操作
  3. 配置JDBC信息
  4. 创建实体类、Mapper接口
  5. 创建UserDetailsService类

创建数据库

创建了一个 mybatis-plus 数据库 ,其中创建了一个users表,记得创建后,加入数据

引入依赖

<!-- Mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
<!-- mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- Lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

配置JDBC信息

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis-plus?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root

创建实体类、Mapper接口

@Data    // 引入了Lombok才可以使用
public class Users {
private Integer id;
private String username;
private String password;
}
@Repository
@Mapper
public interface UsersMapper extends BaseMapper<Users> {
}

创建UserDetailsService类

@Service("userDetailsService")
public class MyuserDetailsService implements UserDetailsService {
@Autowired
private UsersMapper usersMapper; @Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//调用usersMapper方法
QueryWrapper<Users> wrapper = new QueryWrapper<>();
wrapper.eq("username",username);
Users users = usersMapper.selectOne(wrapper);
if (users == null){
//数据库没有用户名,认证失败
throw new UsernameNotFoundException("用户名不存在");
}
List<GrantedAuthority> auths =
AuthorityUtils.commaSeparatedStringToAuthorityList("role");
return new User(users.getUsername(),
new BCryptPasswordEncoder().encode(users.getPassword()),auths);
}
}

这时候就可以正常运行了

5.自定义登录页面

在上面代码的基础上完成该部分代码

1.创建前端页面

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form method="post" action="/user/login">
用户名:<input type="text" name="username">
<br>
密码:<input type="text" name="password">
<br>
<input type="submit" value="login">
</form>
</body>
</html>

2.书写Controller层代码

@GetMapping("/index")
public String index(){
return "index";
}

3.在创建的配置类中重写 configure(HttpSecurity http) 方法

@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() //自定义自己编写的登录页面
.loginPage("/login.html") //设置登陆页面
.loginProcessingUrl("/login") //成功登录访问路径 , 该处路径和from表单中的action路径统一
.defaultSuccessUrl("/index").permitAll() //登录成功之后跳转路径
.and().authorizeRequests()
.antMatchers("/","/hello","/login") //可以直接访问的路径,不需要认证
.permitAll()
.anyRequest().authenticated()
.and().csrf().disable(); //关闭csrf
}

这时候可以分别测试进入以下两个路径:

http://localhost:8081/hello

http://localhost:8081/index

会发现,第一个 hello 路径 ,不会拦截了,可以直接进入页面

第二个index,会进入自定义的登陆页面,登陆成功后,才可以进入

基于角色或权限的访问控制

1.hasAuthority方法

如果当前的主体具有指定的权限,则返回true,否则返回false

  1. 修改配置类
  2. 在 UserDetailsService 实现类中添加权限
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/user/login")
.defaultSuccessUrl("/index").permitAll()
.and().authorizeRequests()
.antMatchers("/","/hello","/user/login")
.permitAll() //当前登录用户,只有具有admins权限才可以访问这个路径
.antMatchers("/index").hasAuthority("admins") .anyRequest().authenticated()
.and().csrf().disable();
}
    @Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<Users> wrapper = new QueryWrapper<>();
wrapper.eq("username",username);
Users users = usersMapper.selectOne(wrapper);
if (users == null){
throw new UsernameNotFoundException("用户名不存在");
} List<GrantedAuthority> auths =
AuthorityUtils.commaSeparatedStringToAuthorityList("admins"); //这里添加权限
return new User(users.getUsername(),
new BCryptPasswordEncoder().encode(users.getPassword()),auths);
}

进行测试,路径为:

http://localhost:8081/index

  • 如果权限不通过 , 403 无权限

  • 如果权限通过 ,正常运行

2.hasAnyAuthority方法

如果当前的主体有任何提供的角色(给定的作为一个逗号分隔的字符串列表)的话,返回 true

与 hasAuthority() 的区别是

  • hasAuthority() 参数唯一,只能满足这一个权限才可以

  • 而该方法,参数可以多个,满足其中一个权限 即为通过

@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/user/login")
.defaultSuccessUrl("/index").permitAll()
.and().authorizeRequests()
.antMatchers("/","/hello","/user/login")
.permitAll() //当前登录用户,具有admins或者user权限才可以访问这个路径
.antMatchers("/index").hasAnyAuthority("admins","user") .anyRequest().authenticated()
.and().csrf().disable();
}

3.hasRole方法

如果用户具备给定角色就 允许访问,否则出现 403

如果当前主体具有指定的角色,则返回 true

该方法与 hasAuthority 方法,使用方法基本相同,区别就是 他需要在权限前加上 ROLE_

@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/user/login")
.defaultSuccessUrl("/index").permitAll()
.and().authorizeRequests()
.antMatchers("/","/hello","/user/login")
.permitAll() //当前登录用户,只有具有 ROLE_user 权限才可以访问这个路径
.antMatchers("/index").hasRole("user") .anyRequest().authenticated()
.and().csrf().disable();
}
// UserDetailsService 实现类中添加权限
List<GrantedAuthority> auths =
AuthorityUtils.commaSeparatedStringToAuthorityList("admins,ROLE_user");

这时候可以正常运行

4.hasAnyRole方法

表示用户具备任何一个条件都可以访问

该方法与 hasRole() 的区别 与1 2 两种方法相同,大家可以自行测试

5.自定义403页面

1.创建自定义403页面

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>没有权限访问!!!</h1>
</body>
</html>

2.修改配置类

@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling().accessDeniedPage("/uuauth.html");
}

个人博客为:

MoYu's Github Blog

MoYu's Gitee Blog

Spring Security 上的更多相关文章

  1. 基于Spring Security 的JSaaS应用的权限管理

    1. 概述 权限管理,一般指根据系统设置的安全规则或者安全策略,用户可以访问而且只能访问自己被授权的资源.资源包括访问的页面,访问的数据等,这在传统的应用系统中比较常见.本文介绍的则是基于Saas系统 ...

  2. Spring Security(三十五):Part III. Testing

    This section describes the testing support provided by Spring Security. 本节介绍Spring Security提供的测试支持. ...

  3. Spring Security 5.0.x 参考手册 【翻译自官方GIT-2018.06.12】

    源码请移步至:https://github.com/aquariuspj/spring-security/tree/translator/docs/manual/src/docs/asciidoc 版 ...

  4. 【JavaEE】SSH+Spring Security基础上配置AOP+log4j

    Spring Oauth2大多数情况下还是用不到的,主要使用的还是Spring+SpringMVC+Hibernate,有时候加上SpringSecurity,因此,本文及以后的文章的example中 ...

  5. Spring Security中异常上抛机制及对于转型处理的一些感悟

    在使用Spring Security的过程中,我们会发现框架内部按照错误及问题出现的场景,划分出了许许多多的异常,但是在业务调用时一般都会向外抛一个统一的异常出来,为什么要这样做呢,以及对于抛出来的异 ...

  6. Spring Security OAuth2 开发指南

    官方原文:http://projects.spring.io/spring-security-oauth/docs/oauth2.html 翻译及修改补充:Alex Liao. 转载请注明来源:htt ...

  7. SPRING SECURITY JAVA配置:Web Security

    在前一篇,我已经介绍了Spring Security Java配置,也概括的介绍了一下这个项目方方面面.在这篇文章中,我们来看一看一个简单的基于web security配置的例子.之后我们再来作更多的 ...

  8. 【OAuth2.0】Spring Security OAuth2.0篇之初识

    不吐不快 因为项目需求开始接触OAuth2.0授权协议.断断续续接触了有两周左右的时间.不得不吐槽的,依然是自己的学习习惯问题,总是着急想了解一切,习惯性地钻牛角尖去理解小的细节,而不是从宏观上去掌握 ...

  9. spring security oauth2.0 实现

    oauth应该属于security的一部分.关于oauth的的相关知识可以查看阮一峰的文章:http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html ...

随机推荐

  1. 项目管理之Git

    @[TOC]( Git命令:分支与合并)Git一款很好的项目版本管理工具,更是一款优秀的分布式项目管理工具.今天主要给大家介绍Git 强大的分支和合并功能,分支和合并可以说在实际的工作当中用到的是最多 ...

  2. Mac创建Root用户

    1.打开Mac终端管理工具 前往-实用工具-终端 2.用命令的形式创建账户 sudo passwd root 3.输入当前登录用户密码 4.输入root用户密码并验证

  3. 关于Laravel框架中Guard的底层实现

    1. 什么是Guard 在Laravel/Lumen框架中,用户的登录/注册的认证基本都已经封装好了,开箱即用.而登录/注册认证的核心就是: 用户的注册信息存入数据库(登记) 从数据库中读取数据和用户 ...

  4. JVM-对象及对象内存布局

    目录 前言 类与对象 对象类二分模型 对象 对象内存布局 JOL工具 对象头 Mark Word 类型句柄 对象头与锁膨胀 无锁 偏向锁 轻量级锁 重量级锁 重量级锁降级 实例数据 填充 对象生命周期 ...

  5. STM32 ADC详细篇(基于HAL库)

    一.基础认识 ADC就是模数转换,即将模拟量转换为数字量 l  分辨率,读出的数据的长度,如8位就是最大值为255的意思,即范围[0,255],12位就是最大值为4096,即范围[0,4096] l  ...

  6. super_curd组件技术点总结

    1.基于包的导入的方式实现单例模式 # test1.py class AdminSite(object): def __init__(self): self.registry = {} self.ap ...

  7. 浅谈Dotnet的数据定位和匹配

    Dotnet里,数据定位和匹配的相关编程现在变得很舒服.   最近项目紧,还要不停出差. 所以,写个短点的.最近经常用到的内容:数据定位和匹配.   数据定位 假设我们有这样一个数组: var arr ...

  8. WPF 应用 - 通过 js 缩放 System.Windows.Controls.WebBrowser 的内容

    1. 前提 原本是在大屏上展示系统,系统有个功能是加载第三方的网站,第三方网站按照大屏的分辨率写死了宽高: 现需要改到小屏展示系统,而这个第三方的网站不能随着 WebBrowser 窗口的尺寸调整网站 ...

  9. (数据科学学习手札112)Python+Dash快速web应用开发——表单控件篇(上)

    本文示例代码已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 这是我的系列教程Python+Dash快速web ...

  10. Codeforces Round #538 D. Lunar New Year and a Wander

    题面: 传送门 题目描述: Bob想在公园散步.公园由n个点和m条无向边组成.当Bob到一个未经过的点时,他就会把这个点的编号记录在笔记本上.当且仅当Bob走完所有的点,他才会停下来.这时,Bob的笔 ...