SpringSecurity
SpringSecurity
简介
-
简介
SpringSecurity
是一个功能强大且高度可定制的身份验证和访问控制框架。 SpringSecurity
致力于为 Java
应用程序提供身份验证和授权的能力。像所有 Spring
项目一样, SpringSecurity
的真正强大之处在于它可以轻松扩展以满足定制需求的能力 -
核心功能
- 用户认证
( Authentication
): 验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程 - 用户授权
( Authorization
): 验证某个用户是否具有权限操作执行某个操作。在一个系统中,不同用户所有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,有的用户既能读取,又能修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色对应一系列的权限
- 用户认证
创建起步初识项目
-
添加依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</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-security</artifactId>
</dependency>
</dependencies> -
版本信息
版本确定 -
创建控制器
1
2
3
4
5
6
7
8
public class HelloController {
public String hello() {
return "Hello SpringSecurity";
}
} -
启动访问
添加 security
,访问会出现如下页面 用户名: user
,密码来自于控制台
过滤器链
-
调式查看
客户端->
目标资源 -
功能介绍
WebAsyncManagerIntegrationFilter
: 将SecurityContext
集成到 SpringMVC
中用于管理异步请求处理的 WebAsyncManager
中 SecurityContextPersistenceFilter
: 在当前会话中填充SecurityContext
,SecurityContext
即 Security
的上下文对象,里面包含了当前用户的认证及权限信息等 HeadWriteFilter
: 向请求的Header
中添加信息 LogoutFilter
:匹配 URL
为 /logout
的请求,清除认证信息, 实现用户注销功能 UsernamePasswordAuthenticationFilter
: 认证操作的过滤器,用于匹配 URL
为 /login
的 POST
请求做拦截, 校验表单的用户名和密码 RequestCacheAwareFilter
: 用于缓存HttpServletRequest
SecurityContextHolderAwareRequestFilter
: 用于封装ServletRequest
,让 ServletRequest
具备更多功能 AnonymousAuthenticationFilter
: 对于未登录情况下的处理,当 SecurityContextHolder
中认证信息为空, 则会创建一个匿名用户存入到 SecurityContextHolder
中 SessionManagementFilter
: 限制统一用户开启多个会话ExceptionTranslationFilter
: 异常过滤器,用来处理在认证授权过程中抛出的异常 FilterSecurityInterceptor
: 获取授权信息,根据SecurityContextHolder
中存储的用户信息判断用户是否有权限访问
-
过滤器的加载过程
SpringBoot
在整合 SpringSecurity
项目时会自动配置 DelegatingFilterProxy
过滤器, 若非 Springboot
工程、则需要手动配置该过滤器 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public 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);
}
认证方式
认证的概念
所谓的认证,
就是用来判断系统中是否存在某用户, 并判断用户的身份是否是合法的过程,解决的其实是用户登录的问题,认证的存在,是为了保护系统中的隐私数据与资源,只有合法的用户才可以访问系统中的资源
HTTP
基本认证
-
基本认证
HTTP
基本认证是在 RFC2616
标准中定义的一种认证模式,它以一种很简单的方式与用户进行交互。 HTTP
基本认证可以分为如下 4
个步骤 - 客户端首先发起一个未携带认证信息的请求
- 然后服务端返回一个
401 Unauthorized
的响应信息, 并在 WWW-Authentication
头部中说明认证形式: 当进行 HTTP
基本认证时, WWW-Authentication
会被设置为 Basic relam="被保护的页面"
- 接下来客户端会收到
401 Unauthorized
响应信息, 并弹出一个对话框, 询问用户名和密码。当用户输入后,客户端会将用户名和密码使用 冒号
进行拼接并用Nasr64
编码, 然后将其放入到请求的 Authorization
头部并发送给服务器 - 最后服务器对客户端发来的信息进行解码得到用户名和密码,
并对该信息进行校验判断是否正确, 最终给客户端返回响应内容
HTTP
基本认证是一种无状态的认证方式, 与表单认证相比, HTTP
基本认证是一种基于 HTTP
层面的认证方式,无法携带 Session
信息, 也就无法实现 Remember-Me
功能,另外,用户名和密码在传递时仅做了一次简单的 Base64
编码,几乎等同于明文传输,极易被进行密码窃听和重放攻击。所以在实际开发中, 很少会使用这种认证方式来进行安全校验。
Form
表单认证
HTTP
摘要认证
-
概念
HTTP
摘要认证和 HTTP
基本认证一样, 也是在 RFC2616
中定义的一种认证方式,他的出现是为了弥补 HTTP
基本认证存在的安全隐患,但该认证方式也并不是很安全. HTTP
摘要认证会使用对通信双方都可知的口令进行校验,且最终以密文的形式来传输数据,所以相对于基本认证, 稍微安全了一些
登录页-自定义用户名和密码
-
配置
1
2
3
4
5
6spring:
security:
user:
name: coder-itl
# setPassword
password: root1
2
3
4
5
6
7
8// password: root 的源码
public void setPassword(String password) {
if (StringUtils.hasLength(password)) {
// 控制台生成密码的开关
this.passwordGenerated = false;
this.password = password;
}
}
关闭验证功能
-
排除
Secuirty
的配置, 让他不启用 1
2
3
4
5
6
7
8// 启动类
public class SpringSecurity01Application {
public static void main(String[] args) {
SpringApplication.run(SpringSecurity01Application.class, args);
}
}
登录页-使用内存中的用户信息
-
使用:
WebSecurityConfigurerAdapter
控制安全管理的内容 需要做的使用: 继承
WebSecurityConfigurerAdapter
,重写方法。实现自定义的认证信息 -
创建
security
的配置类 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25package com.example.config;
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
// 在方法中配置 用户名和密码信息 作为登录的数据
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
PasswordEncoder passwordEncoder = passwordEncoder();
auth.inMemoryAuthentication()
.withUser("coder-itl").password(passwordEncoder.encode("coder-itl")).roles();
auth.inMemoryAuthentication()
.withUser("admin").password(passwordEncoder.encode("admin")).roles();
auth.inMemoryAuthentication()
.withUser("root").password(passwordEncoder.encode("root")).roles();
}
// 创建密码的加密类
public PasswordEncoder passwordEncoder() {
// 创建 PasswordEncoder 的实现类 实现类是加密算法
return new BCryptPasswordEncoder();
}
}错误: 密码不能为明文信息,解决方法为添加加密
-
基于角色
Role
的身份认证,同一个用户可以有不同的角色。同时可以开启对方法级别的认证 1
2
3@EnableGlobalMethodSecurity: 启用方法级别的认证
prePostEnabled: 默认是 false
true: 表示可以使用 @PreAuthorize 注解 和 @PostAuthorize1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23// 控制器编写: 基于内存中的用户信息认证
public class HelloController {
public String hello() {
return "Hello SpringSecurity";
}
// 指定 normal 和 admin 都可以访问的方法
public String helloCommonUser() {
return "normal 和 admin 都可以访问的方法";
}
// 仅 admin 可以访问的方法
public String helloAdmin() {
return "仅 admin 可以访问的方法";
}
} -
细节注意
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70package com.example.oauth2.config;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private PasswordEncoder passwordEncoder;
protected void configure(HttpSecurity http) throws Exception {
/**
* authorizeRequests: 授权请求
* authenticated: 认证
*/
http.authorizeRequests().anyRequest().authenticated().and()
// 开启表单验证
.formLogin().loginPage("/login.html").permitAll()
// 当登录成功后,是否指定跳转到首页
.defaultSuccessUrl("/index.html", true)
// post 请求的登录接口
.loginProcessingUrl("/login")
// 登录失败,用户名或密码错误
.failureUrl("/error.html")
// 登录时携带的用户名和免密的表单的键
.usernameParameter("username").passwordParameter("password")
// 注销
.and().logout().logoutUrl("/logout")
// 注销成功后的页面
.logoutSuccessUrl("/login.html")
// 删除自定义的 cookie
.deleteCookies("myCookie")
// 关闭 csrf 防护功能,否则登录不成功
.and().csrf().disable();
}
/**
* 配置请求那些资源时不需要做认证
*
* @param web
* @throws Exception
*/
public void configure(WebSecurity web) throws Exception {
web.ignoring().mvcMatchers("/js/**", "/css/**", "/image/**");
}
/* 基于内存的用户名和密码 */
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// TODO: 如果注入 passwordEncoder 会产生循环依赖,需要通过配置来进行打破循环依赖
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 生成密码的密文
String pwd = passwordEncoder.encode("user");
// 设置用户名和密码
auth.inMemoryAuthentication().withUser("user").password(pwd).roles("admin");
}
}1
2
3
4
5# 当注入了 PasswordEncoder 时
spring:
main:
# 打破循环依赖
allow-circular-references: true
角色- RBAC
-
认证
身份认证
就是判断一个用户是否为合法用户的处理过程。Spring Security
中支持多种不同方式的认证,但是无论开发者使用那种方式认证,都不会影响授权功能使用。因为 Spring Security
很好的做到了认证和授权解饿 -
授权
即访问控制,控制谁能访问哪些资源。简单的理解授权就是根据系统提前设置好的规则,给用户分配可以访问某一个资源的权限,用户根据自己所具有的权限,去执行响应操作。
-
RBAC
RBAC 是基于角色的访问控制
( Role-Based Access Controle
),在 RBAC 中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予给角色,而把角色又赋予给用户 其基本思想是对系统操作的各种权限不是直接授予具体的用户,而是在用户集合之间建立一个角色集合。每一种角色对应一组相应的权限。一旦被用户分配到了适当的角色后,该用户就拥有此角色的所有操作权限。这样做的好处是,不必在每次创建用户时都进行分配权限的操作,只要分配用户相应的角色即可,而且角色的权限变更比用户的权限变更要少得多,这样就简化用户的权限管理,减少系统的开销
-
RBAC 中设计表
- 用户表: 用户认证
(登录用到的表),用户名、密码、是否启用,是否锁定等信息 - 角色表: 定义角色信息,角色名称、角色的描述
- 用户和角色的关系表:用户和角色是多对多的关系。一个用户可以又多个角色,一个角色可以有多个用户
- 权限表:角色和权限的关系表,权限
(角色可以有那些权限)
- 用户表: 用户认证
认证的接口和类
-
UserDetails
接口 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface UserDetails extends Serializable {
// 权限的集合
Collection<? extends GrantedAuthority> getAuthorities();
// 密码
String getPassword();
// 用户名
String getUsername();
// 账号是否过期
boolean isAccountNonExpired();
// 账号是否锁定
boolean isAccountNonLocked();
// 证书是否过期
boolean isCredentialsNonExpired();
// 是否启用
boolean isEnabled();
} -
UserDetails
的实现类 User
1
2
3
4import org.springframework.security.core.userdetails.User;
// 自定义类可以实现 UserDetails 接口,作为系统中的用户类。这个类可以交给 spring security 使用 User
-
UserDetailsService
接口 主要作用: 获取用户信息,得到的是
UserDetails
对象。一般项目中都需要自定义类实现这个接口,从数据库中获取数据 1
2
3
4
5// 一个方法要实现
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 根据用户名,获取用户信息
}
自动配置分析
-
自动配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static class SecurityFilterChainConfiguration {
SecurityFilterChainConfiguration() {
}
// 创建过滤器 SecurityFilterChain
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
((ExpressionUrlAuthorizationConfigurer.AuthorizedUrl)http.authorizeRequests().anyRequest()).authenticated(); // 表单认证
http.formLogin();
// 早期的认证方式
http.httpBasic();
return (SecurityFilterChain)http.build();
}
}
生成默认登录页的流程分析
-
流程图
流程分析 - 关键说明
- 请求
/hello
接口, 在引入 spring security
之后会经过一系列过滤器 - 在请求到达
FilterSecurityInterceptor
时, 发送请求并未认证。请求拦截下来,并抛出 AccessDeniedException
异常 - 抛出
AccessDeniedException
的异常会被 ExceptionTranslationFilter
捕获,这个 Filter
中会调用 LoginUrlAuthenticationEntryPoint#commence
方法返回给客户端 302(重定向)
,要求客户端进行重定向到 /login
页面 - 客户端发送
/login
请求 /login
请求会再次被拦截其中 DefaultLoginPageGeneratingFilter
拦截到, 并在拦截其中返回生成登录页面
- 请求
- 关键说明
自定义认证
-
需求
index
公共资源 hello
受限资源
-
控制器
1
2
3
4
5
6
7
8
9
10
11/**
* 受保护资源
*/
public class HelloController {
public String hello() {
return "hello spring security";
}
}1
2
3
4
5
6
7
8
9
10
11/**
* 公共资源
*/
public class IndexController {
public String index() {
return "hello index.........";
}
} -
创建配置类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// WebSecurityConfigurerAdapter 在 2.7 过时
public class WebSecurityConfigure extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
// 放行资源在前 首先资源在后
http.authorizeRequests()
// 匹配放行资源
.mvcMatchers("/index").permitAll()
// 其他皆是受保护资源
.anyRequest().authenticated().and().formLogin();
}
}1
2
3
4# 说明
- permitAll() 代表放行该资源 该资源为公共资源 无需认证和授权可以直接访问
- anyRequest().authenticated() 代表所有请求,必须认证后才能访问
- formLogin() 代表开启表单认证
自定义登录页面
-
vue
页面 自定义页面 -
修改默认访问页面
1
2// 配置类后续追加该配置 指定默认的登录页面
.loginPage("http://localhost:8081/#/login");
-
-
Thymeleaf
使用 -
添加依赖
1
2
3
4<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency> -
关闭缓存
1
2
3spring:
thymeleaf:
cache: false -
自定义登录页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>自定义用户登录页面 </title>
<style>
form {
background-color: orange;
}
</style>
</head>
<body>
<form th:action="@{/login}" method="post">
<p>
用户名: <input type="text" name="username">
</p>
<p>
密码: <input type="password" name="password">
</p>
<input type="submit" value="登录">
</form>
</body>
</html> -
配置类中放行请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// WebSecurityConfigurerAdapter 在 2.7 过时
public class WebSecurityConfigure extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
// 放行资源在前 首先资源在后
http.authorizeRequests()
// 匹配放行资源
.mvcMatchers("/login").permitAll()
.mvcMatchers("/index").permitAll()
// 其他皆是受保护资源
.anyRequest().authenticated().and().formLogin()
// 指定默认的登录页面 一旦自定义登录页面以后必须只能登录 url
.loginPage("/login")
// 指定处理登录请求 url
.loginProcessingUrl("/login")
// 指定input name 属性值如果不是 username 和 password 时
.usernameParameter("username")
.passwordParameter("password")
// 传统开发: 认证成功后跳转(只能选其一) forward(不改变路由): 始终在认证成功后跳转到指定请求 | redirect(改变路由) 根据上一次保存的请求进行成功跳转, 可通过修改参数强制跳转 .defaultSuccessUrl("/index",true)
.successForwardUrl("/index")
.defaultSuccessUrl("/index")
// 禁止 csrf 跨站请求保护
.and().csrf().disable();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// 前后端分离
/**
* 自定义认证成功之后的处理
*/
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Map<String, Object> result = new HashMap<>();
result.put("msg", "登录成功");
result.put("status", "200");
result.put("authentication", authentication);
response.setContentType("application/json;charset=UTF-8");
String json = new ObjectMapper().writeValueAsString(result);
response.getWriter().println(json);
}
}1
2// 配置类添加如下配置
.successHandler(new MyAuthenticationSuccessHandler())页面响应 json
数据 -
控制器
1
2
3
4
5
6
7
8
public class LoginController {
public String login() {
// login.html
return "login";
}
} -
获取失败信息
1
2
3
4
5
6
7
8
9// 配置类中
// forwald 错误信息存储在 request 作用域
.failureForwardUrl("/login")
// redirect 错误信息村粗在 session 作用域
.failureUrl("/login")
存储的键: SPRING_SECURITY_LAST_EXCEPTION -
前后端错误信息处理
1
.failureHandler(new MyAuthenticationFailureHandler())
1
2
3
4
5
6
7
8
9
10
11
12
13
14/**
自定义认证失败
*/
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
Map<String, Object> result = new HashMap<>();
result.put("msg", "登录失败: " + exception.getMessage());
result.put("status", 500);
response.setContentType("application/json;charset=UTF-8");
String json = new ObjectMapper().writeValueAsString(result);
response.getWriter().println(json);
}
}输入错误的密码
-
注销
-
配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14.and()
// logoutUrl | logoutRequestMatcher 选其一
.logout()
.logoutUrl("/logout")
// 自定义退出路径与请求方式
.logoutRequestMatcher(new OrRequestMatcher(
new AntPathRequestMatcher("/aa","GET"),
// POST 请求的退出
new AntPathRequestMatcher("/bb","POST")
))
// 退出成功后跳转的页面 默认/login
.logoutSuccessUrl("/login")
.logoutSuccessHandler(new MyLogoutSuccessHandler())
-
前端后端分离
1
2
3
4
5
6
7
8
9
10
11public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Map<String, Object> result = new HashMap<>();
result.put("msg", "注销成功,当前认证对象为: " + authentication);
result.put("status", "200");
response.setContentType("application/json;charset=UTF-8");
String json = new ObjectMapper().writeValueAsString(result);
response.getWriter().println(json);
}
}自定义注销
完整配置顺序
-
顺序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 安全管理配置
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 所有请求
.anyRequest()
// 都需要认证
.authenticated()
// 认证方式为表单认证
.and().formLogin()
// 登录成功后
.successHandler(new MyAuthenticationSuccessHandler())
// 登录失败后
.failureHandler(new MyAuthenticationFailureHandler())
.and().logout().logoutUrl("/logout")
.and()
.logout().logoutUrl("/logout")
// 退出成功后
.logoutSuccessHandler(new MyLogoutSuccessHandler())
// 禁用跨站
.and().csrf().disable();
}
}
WebSecurity 和 HttpSecurity
-
配置
1
2
3
4
5
6
7
8
9
10
11
12
13/**
* 配置请求那些资源时不需要做认证
*
* @param web
* @throws Exception
*/
public void configure(WebSecurity web) throws Exception {
web.ignoring().mvcMatchers("/js/**", "/css/**", "/image/**");
}
protected void configure(HttpSecurity http) throws Exception {} -
WebSecurity
: 在这个类里定义了一个securityFilterChainBuilders
集合,可以同时管理多个 SecurityFilterChain
过滤器链,当 WebSecurity
在执行时,会构建出一个名为 springSecurityFilterChain
的 Spring BeanFilterChainProxy
代理类,他的作用是定义那些请求客户忽略安全控制,那些请求必须接受安全控制,以及在合适的时候 清除 SecurityContext
以避免内存泄漏,同时也可以用来 定义请求防火墙和请求拒绝处理器
,也可以在这里开启 Spring Security
的 Debug
模式 -
HttpSecurity
:HttpSecurity
用来构建包含一系列的过滤器链 SecurityFilterChain
,平常我们的配置就是围绕这个SecurityFilterChain
进行
自定义数据源认证 (一)
-
用户表
1
2
3
4
5
6
7
8
9
10
11
12
13create table sys_user
(
id int not null primary key auto_increment comment '主键id',
username varchar(100) comment '用户名',
password varchar(100) comment '密码',
realname varchar(200) comment '实名用户名',
isexpire int comment '是否过期',
isenable int comment '是否禁用',
islock int comment '是否锁定',
iscredentials int comment '证书是否可用',
createtime date comment '创建时间',
logintime date comment '登录时间'
) comment '用户表'; -
角色表
1
2
3
4
5
6
7create table sys_role
(
id int not null primary key auto_increment,
rolename varchar(255) comment '角色名称',
rolememo varchar(255) comment '角色描述'
) comment '角色表'; -
角色用户对应关系表
1
2
3
4
5create table sys_user_role
(
userid int,
roleid int
) comment '用户角色关系对应表';表信息 -
创建项目
-
添加依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</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.2.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</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> -
配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26server:
port: 8081
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
url: jdbc:mysql://localhost:3306/security?useUnicode=true&serverTimezone=GMT&characterEncoding=UTF-8&useSSL=false
security:
user:
name: root
password: root
mybatis:
type-aliases-package: com.example.entity
mapper-locations: com/example/mapper/*.Mapper
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.example.mapper: debug -
自定义
User
实现 UserDetails
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62package com.example.entity;
public class SysUser implements UserDetails {
private Integer id;
private String username;
private String password;
private String realname;
// 是否过期
private boolean isExpired;
// 是否锁定
private boolean isLocked;
// 凭证是否有效
private boolean isCredentials;
// 是否启用
private boolean isEnabled;
private Date createTime;
private Date loginTime;
private List<GrantedAuthority> authorities;
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
public String getPassword() {
return password;
}
public String getUsername() {
return username;
}
public boolean isAccountNonExpired() {
return isExpired;
}
public boolean isAccountNonLocked() {
return isLocked;
}
public boolean isCredentialsNonExpired() {
return isCredentials;
}
public boolean isEnabled() {
return isEnabled;
}
} -
角色
1
2
3
4
5
6
7
8
9
10
11package com.example.entity;
public class SysRole {
private Integer id;
private String rolename;
private String rolememo;
} -
实现
service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38package com.example.service;
public class JdbcUSerServiceDetail implements UserDetailsService {
private SysUserMapper sysUserMapper;
private SysRoleMapper sysRoleMapper;
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("username: [{}]", username);
SysUser sysUser = null;
// 角色信息集合
List<GrantedAuthority> authorities = new ArrayList<>();
// 1. 根据用户名查询用户信息
if (username != null) {
sysUser = sysUserMapper.selectSysUser(username);
log.error("sysUser: [{}]", sysUser.toString());
String roleName = "";
if (sysUser != null) {
List<SysRole> roleList = sysRoleMapper.selectRoleByUser(sysUser.getId());
log.error("roleList: [{}]", roleList.toString());
for (SysRole role : roleList) {
roleName = role.getRolename();
GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_" + roleName);
authorities.add(authority);
}
}
sysUser.setAuthorities(authorities);
}
return sysUser;
}
} -
Security
配置类 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31package com.example.config;
import javax.annotation.Resource;
public class CustomerWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
private UserDetailsService userDetailsService;
protected void configure(HttpSecurity http) throws Exception {
log.error("config HttpSecurity...........");
http.authorizeRequests().antMatchers("/index").permitAll()
.antMatchers("/access/read").hasRole("READ")
.antMatchers("/access/user").hasRole("USER")
.antMatchers("/access/admin").hasRole("ADMIN")
// 其他皆是受保护资源
.anyRequest().authenticated().and().formLogin();
}
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 实现自定义 new BCryptPasswordEncoder()
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
} -
创建
mapper
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<mapper namespace="com.example.mapper.SysUserMapper">
<resultMap id="userMap" type="sysUser">
<id column="id" property="id"/>
<!-- column: 数据库列明 property: 实体字段名 -->
<result column="username" property="username"/>
<result column="password" property="password"/>
<result column="realname" property="realname"/>
<result column="isexpire" property="isExpired"/>
<result column="isenable" property="isEnabled"/>
<result column="islock" property="isLocked"/>
<result column="iscredentials" property="isCredentials"/>
<result column="createtime" property="createTime"/>
<result column="logintime" property="loginTime"/>
</resultMap>
<!-- int insertSysUser(SysUser user); -->
<insert id="insertSysUser" parameterType="sysUser">
insert into sys_user(username,
password,
realname,
isenable,
islock,
isexpire,
iscredentials,
createtime,
logintime)
values (#{username},
#{password},
#{realname},
#{isEnabled},
#{isLocked},
#{isExpired},
#{isCredentials},
#{createTime},
#{loginTime})
</insert>
<!-- SysUser selectSysUser(String username); -->
<select id="selectSysUser" parameterType="string" resultMap="userMap">
select *
from sys_user
where username = #{username}
</select>
</mapper>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<mapper namespace="com.example.mapper.SysRoleMapper">
<!-- List<SysRole> selectRoleByUser(Integer userId); -->
<select id="selectRoleByUser" parameterType="integer" resultType="sysRole">
select r.id, r.rolename, r.rolememo
from sys_user_role ur,
sys_role r
where ur.roleid = r.id
and ur.userid = #{userId}
</select>
</mapper> -
初始化信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
public void init() {
PasswordEncoder pwdEncoder = new BCryptPasswordEncoder();
Date curDate = new Date();
List<GrantedAuthority> list = new ArrayList<>();
// 参数角色名称 必须以"ROLE_"开头 后面加上自定义的角色名称
// GrantedAuthority read = new SimpleGrantedAuthority("ROLE_" + "READ");
// GrantedAuthority user = new SimpleGrantedAuthority("ROLE_" + "USER");
GrantedAuthority admin = new SimpleGrantedAuthority("ROLE_" + "ADMIN");
// list.add(read);
// list.add(user);
// list.add(admin);
// 普通用户
// SysUser lisiRead = new SysUser(1, "lisi", pwdEncoder.encode("lisi"), "李四", true, true, true, true, curDate, curDate, list);
// SysUser zsUser= new SysUser(2, "zs", pwdEncoder.encode("zs"), "张三", true, true, true, true, curDate, curDate, list);
SysUser coderitlAdmin = new SysUser(3, "coder-itl", pwdEncoder.encode("coder-itl"), "CODER-ITL", true, true, true, true, curDate, curDate, list);
// sysUserMapper.insertSysUser(lisiRead);
// sysUserMapper.insertSysUser(zsUser);
sysUserMapper.insertSysUser(coderitlAdmin);
} -
源文件
-
静态页面跳转
1
2
3
4
5
6
7
public class IndexController {
public String index() {
return "forward:/index.html";
}
}index
实现跳转 -
静态页面
1
2
3
4
5
6
7
8
9
10
11
12
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<a href="/access/read">只读-李四 </a>
<a href="/access/user">普通用户-张三 </a>
<a href="/access/admin">CODER-ITL 管理员</a>
</body>
</html> -
控制器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26package com.example.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
public class MyController {
public String accessZS() {
return "zs and admin.....";
}
public String accessRD() {
return "lisi.....";
}
public String accessAM() {
return "admin.....";
}
}
-
-
自定义数据源认证 (二)
-
spring-security
中的 User
User
-
设计表结构
1
2
3
4
5
6
7
8
9
10
11
12-- 用户表
CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` varchar(32) DEFAULT NULL COMMENT '用户名',
`password` varchar(255) DEFAULT NULL COMMENT '密码',
`enabled` tinyint(1) DEFAULT NULL COMMENT '是否启用',
`accountNonExpired` tinyint(1) DEFAULT NULL COMMENT '账户是否过期',
`accountNonLocked` tinyint(1) DEFAULT NULL COMMENT '账户是否锁定',
`credentialsNonExpired` tinyint(1) DEFAULT NULL COMMENT '凭证是否过期',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci1
2
3
4-- 插入用户数据
insert into user values (1, 'root', '{noop}123', 1, 1, 1, 1)
insert into user values (2, 'admin', '{noop}123', 1, 1, 1, 1)
insert into user values (3, 'coder-itl', '{noop}123', 1, 1, 1, 1)1
2
3
4
5
6CREATE TABLE `role` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '角色ID',
`name` varchar(32) DEFAULT NULL COMMENT '角色名称',
`name_zh` varchar(32) DEFAULT NULL COMMENT '角色中文名称',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci1
2
3insert into role values (1,'ROLE_product','商品管理员');
insert into role values (2,'ROLE_admin','系统管理员');
insert into role values (3,'ROLE_user','用户管理员');1
2
3
4
5
6
7
8CREATE TABLE `user_role` (
`id` int NOT NULL AUTO_INCREMENT,
`uid` int DEFAULT NULL,
`rid` int DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `uid` (`uid`),
KEY `rid` (`rid`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci1
2
3
4insert into user_role values (1, 1, 1);
insert into user_role values (2, 1, 2);
insert into user_role values (3, 2, 2);
insert into user_role values (4, 3, 3);表结构 -
自定义
User
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66package com.example.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.*;
public class User implements UserDetails {
private Integer id;
private String username;
private String password;
private Boolean enabled;
private Boolean accountNonExpired;
private Boolean accountNonLocked;
private Boolean credentialsNonExpired;
private List<Role> roles = new ArrayList<>();
// 权限集合
public Collection<? extends GrantedAuthority> getAuthorities() {
Set<SimpleGrantedAuthority> authorities = new HashSet<>();
roles.forEach(role -> {
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getName());
authorities.add(simpleGrantedAuthority);
});
return authorities;
}
public String getPassword() {
return password;
}
public String getUsername() {
return username;
}
public boolean isAccountNonExpired() {
return accountNonExpired;
}
public boolean isAccountNonLocked() {
return accountNonLocked;
}
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
public boolean isEnabled() {
return enabled;
}
} -
Role
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15package com.example.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
public class Role {
private Integer id;
private String name;
private String nameZh;
} -
mapper
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<mapper namespace="com.example.mapper.UserMapper">
<!-- User findUserByUserName(String username); -->
<select id="findUserByUserName" resultType="user">
select *
from user
where username = #{username}
</select>
<!-- List<Role> getRoleByUid(Integer uid); -->
<select id="getRoleByUid" resultType="role">
select r.id, r.name, r.name_zh
from role r,
user_role ur
where r.id = ur.uid
and ur.uid = #{uid}
</select>
</mapper> -
UserDetailsService
实现 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37package com.example.service;
import com.example.entity.Role;
import com.example.entity.User;
import com.example.mapper.UserMapper;
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 org.springframework.util.ObjectUtils;
import java.util.List;
public class MyUserDetailsService implements UserDetailsService {
private final UserMapper userMapper;
public MyUserDetailsService(final UserMapper userMapper) {
this.userMapper = userMapper;
}
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 根据用户名查询用户
User user = userMapper.findUserByUserName(username);
// 如果用户为空
if (ObjectUtils.isEmpty(user)) throw new UsernameNotFoundException("用户名错误!");
// 查询权限信息
List<Role> roles = userMapper.getRoleByUid(user.getId());
user.setRoles(roles);
return user;
}
} -
配置
SpringSecurity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43package com.example.config;
import com.example.service.MyUserDetailsService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.WebSecurityConfigurerAdapter;
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private MyUserDetailsService myUserDetailsService;
public SecurityConfig(final MyUserDetailsService myUserDetailsService) {
this.myUserDetailsService = myUserDetailsService;
}
// 自定义 AuthenticationManager 推荐 并没有在工厂在暴露出来
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
log.error("自定义 AuthenticationManager: " + builder);
builder.userDetailsService(myUserDetailsService);
}
// 作用: 用来将自定义 AuthenticationManager 在工厂中进行暴露 可以在任意位置注入
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated().and().formLogin().and().csrf().disable();
}
}
传统 WEB 开发认证总结案例
-
SpringMVC
的自定义配置 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18package com.example.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 对 springmvc 进行自定义配置: 可以省略部分控制器
*/
public class MvcConfigure implements WebMvcConfigurer {
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("login");
registry.addViewController("/index").setViewName("index");
}
} -
传统
web
的 Security
所有配置 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33package com.example.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
public class SecurityConfigure extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
// 开启认证
http.authorizeRequests()
// 放行的资源
.mvcMatchers("/login", "/index").permitAll()
// 所有请求都需要认证
.anyRequest().authenticated()
// 表单认证 指定自定义登录页面
.and().formLogin().loginPage("/login")
.loginProcessingUrl("/login")
// 指定用户名 和 密码的 name 属性值
.usernameParameter("username")
.passwordParameter("password")
// 登录成功后跳转的页面
.defaultSuccessUrl("/index")
// 退出
.and().logout()
// 退出成功后的跳转(默认 login)
.logoutSuccessUrl("/login")
.and().csrf().disable();
}
}
前后端分离认证总结
-
配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113package com.example.config;
import com.example.filter.LoginFilter;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationManager;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import java.util.HashMap;
import java.util.Map;
public class SecurityConfigure extends WebSecurityConfigurerAdapter {
// 使用内存中的用户认证
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("admin").build());
return inMemoryUserDetailsManager;
}
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService());
}
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
// 自定义 filter 交给工厂管理
public LoginFilter loginFilter() throws Exception {
LoginFilter loginFilter = new LoginFilter();
// 接受指定的 json 用户名 key
loginFilter.setUsernameParameter("username");
// 接受指定的 json 密码 key
loginFilter.setPasswordParameter("password");
// 注入 authdicationManager
loginFilter.setAuthenticationManager(authenticationManagerBean());
// 认证成功后
loginFilter.setAuthenticationSuccessHandler((req, resp, auth) -> {
Map<String, Object> result = new HashMap<>();
result.put("msg", "登录成功");
// 状态码: 200
resp.setStatus(HttpStatus.OK.value());
result.put("authentication", auth);
resp.setContentType("application/json;charset=UTF-8");
String json = new ObjectMapper().writeValueAsString(result);
resp.getWriter().println(json);
});
// 认证失败后
loginFilter.setAuthenticationFailureHandler((req, resp, ex) -> {
Map<String, Object> result = new HashMap<>();
result.put("msg", "登录失败: " + ex.getMessage());
// 服务器内部错误 状态码: 500
resp.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
resp.setContentType("application/json;charset=UTF-8");
String json = new ObjectMapper().writeValueAsString(result);
resp.getWriter().println(json);
});
return loginFilter;
}
// 配置安全
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 所有请求都需要认证
.anyRequest().authenticated().and().formLogin()
// 退出
.and()
.exceptionHandling()
.authenticationEntryPoint((req, resp, ex) -> {
Map<String, Object> result = new HashMap<>();
result.put("msg", "请认证之后再去处理....");
result.put("status", HttpStatus.UNAUTHORIZED.value());
resp.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
String json = new ObjectMapper().writeValueAsString(result);
resp.getWriter().println(json);
})
.and()
.logout().logoutUrl("/logout")
// 退出成功后
.logoutSuccessHandler((req, resp, auth) -> {
Map<String, Object> result = new HashMap<>();
result.put("msg", "注销成功");
result.put("authentication", auth.getPrincipal());
resp.setContentType("application/json;charset=UTF-8");
resp.setStatus(HttpStatus.OK.value());
String json = new ObjectMapper().writeValueAsString(result);
resp.getWriter().println(json);
})
// 禁用跨站
.and().csrf().disable();
// at: 用在某个 filter 替换过滤器链中哪个 filter
// before: 放在过滤器链中那个 filter 之前
// after: 放在过滤器链中那个 filter 之后
http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
}
} -
控制器
1
2
3
4
5
6
7
8
public class TestController {
public String test() {
return "test ok...........................";
}
} -
自定义前后端认证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48package com.example.filter;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
/**
* 自定义前后端认证
*/
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
log.error("attemptAuthentication: [{}]", "Override attemptAuthentication...................................");
// 1. 判断请求方式是否是 POST
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
// 2. 判断 数据是否是 JSON 格式
if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
try {
// 将请求体中的数据进行反序列化
Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
String username = userInfo.get(getUsernameParameter());
String password = userInfo.get(getPasswordParameter());
log.error("username: [{}]", username);
log.error("password: [{}]", password);
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// 如果不是 JSON 格式数据,则调用传统方式进行认证
return super.attemptAuthentication(request, response);
}
} -
访问测试
访问成功 访问失败 注销成功 -
更改内存数据源为数据库
-
实现
service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37package com.example.service;
import com.example.entity.Role;
import com.example.entity.User;
import com.example.mapper.UserMapper;
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 org.springframework.util.ObjectUtils;
import java.util.List;
public class MyUserDetailsService implements UserDetailsService {
private final UserMapper userMapper;
public MyUserDetailsService(final UserMapper userMapper) {
this.userMapper = userMapper;
}
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 根据用户名查询用户
User user = userMapper.findUserByUserName(username);
// 如果用户为空
if (ObjectUtils.isEmpty(user)) throw new UsernameNotFoundException("用户名错误!");
// 查询权限信息
List<Role> roles = userMapper.getRoleByUid(user.getId());
user.setRoles(roles);
return user;
}
} -
配置修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20package com.example.config;
public class SecurityConfigure extends WebSecurityConfigurerAdapter {
// 使用数据库中的用户认证
private final MyUserDetailsService myUserDetailsService;
public SecurityConfigure(final MyUserDetailsService myUserDetailsService) {
this.myUserDetailsService = myUserDetailsService;
}
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService);
}
...
} -
UserMapper.java
1
2
3
4
5
6
public interface UserMapper {
User findUserByUserName(String username);
List<Role> getRoleByUid(Integer id);
} -
UserMapper.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<mapper namespace="com.example.mapper.UserMapper">
<!-- User findUserByUserName(String username); -->
<select id="findUserByUserName" resultType="user">
select *
from user
where username = #{username}
</select>
<!-- List<Role> getRoleByUid(Integer uid); -->
<select id="getRoleByUid" resultType="role">
select r.id, r.name, r.name_zh
from role r,
user_role ur
where r.id = ur.uid
and ur.uid = #{uid} -
数据源配置
-
访问测试
调试 loadUserByUsername
前后端只能通过其他工具测试,浏览器不支持 成功访问
-
添加认证验证码
-
添加依赖
1
2
3
4
5<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency> -
自定义验证码生成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111package com.example.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Random;
public class CaptchaController {
// 图像宽度 150px
private int width = 150;
private int height = 50;
// 图片内容起始位置
private int drawY = 30;
// 文字间隔
private int space = 18;
// 验证码文字个数
private int charCount = 6;
// 验证码内容数组
private String[] code = {
"A", "B", "C", "D", "E", "F", "G", "H", "K", "Z", "X", "L", "C", "V", "B", "N", "M", "Q", "W",
"R", "T", "Y", "U", "I", "O", "P", "A", "B", "c", "d", "e", "f", "g", "h", "k", "z", "x",
"l", "c", "v", "b", "n", "m", "q", "w", "r", "t", "y", "u", "i", "o", "p",
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0"
};
public void makeCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 创建一个背景透明的图片 使用 rgb 表示颜色的
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
// 获取画笔
Graphics graphics = image.getGraphics();
// 设置使用的画笔是白颜色
graphics.setColor(Color.white);
// 给 inage 画板都涂成白色的
// fillRect(矩形)(矩形的起始x, 矩形的起始 y,width,height)
graphics.fillRect(0, 0, width, height);
// 将文字内容输出到图片上
// 1. 创建一个字体
Font font = new Font("宋体", Font.BOLD, 16);
graphics.setFont(font);
// 设置画笔颜色
graphics.setColor(Color.black);
// 2. 在画布上添加文字
// graphics.drawString("中", 20, drawY);
int ran = 0;
int len = code.length;
StringBuffer stringBuffer = new StringBuffer("");
for (int i = 0; i < charCount; i++) {
ran = new Random().nextInt(len);
// 使用随机颜色绘制
graphics.setColor(randomColor());
stringBuffer.append(code[ran]);
graphics.drawString(code[ran], (i + 1) * space, drawY);
}
// 将验证码存储到 session
request.getSession().setAttribute("code", stringBuffer);
// 绘制干扰线
for (int i = 0; i < 6; i++) {
graphics.setColor(randomColor());
int dot[] = makeLineDot();
graphics.drawLine(dot[0], dot[1], dot[2], dot[3]);
}
// 设置响应格式
response.setContentType("image/png");
// 缓存设置
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
OutputStream outputStream = response.getOutputStream();
ImageIO.write(image, "png", outputStream);
outputStream.flush();
outputStream.close();
}
/**
* 生成随机颜色
*
* @return
*/
private Color randomColor() {
int r = new Random().nextInt(255);
int g = new Random().nextInt(255);
int b = new Random().nextInt(255);
return new Color(r, g, b);
}
private int[] makeLineDot() {
Random random = new Random();
int x1 = random.nextInt(width / 2);
int y1 = random.nextInt(height);
int x2 = random.nextInt(width);
int y2 = random.nextInt(height);
return new int[]{x1, x2, y1, y2};
}
} -
传统
WEB
验证码添加 -
添加谷歌验证码配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26package com.example.config;
import com.google.code.kaptcha.Producer;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;
public class KaptchaConfig {
public Producer kaptcha() {
Properties properties = new Properties();
properties.setProperty("kaptcha.image.width", "150");
properties.setProperty("kaptcha.image.height", "50");
properties.setProperty("kaptcha.textproducer.char.string", "0123456789");
properties.setProperty("kaptcha.textproducer.char.length", "4");
Config config = new Config(properties);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
} -
自定义过滤器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56package com.example.filter;
import com.example.exception.KaptchaNotMatchException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.util.ObjectUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
/**
* 自定义前后端认证
*/
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
private static final String FORM_KAPTCHA_KEY = "code";
private String kaptchaParameter = FORM_KAPTCHA_KEY;
public String getKaptchaParameter() {
return this.kaptchaParameter;
}
public void setKaptchaParameter(final String kaptchaParameter) {
this.kaptchaParameter = kaptchaParameter;
}
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
log.error("attemptAuthentication: [{}]", "Override attemptAuthentication...................................");
// 1. 判断请求方式是否是 POST
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
// TODO: 先进行验证码校验
// 获取请求中的验证码
String parameter = request.getParameter(getKaptchaParameter());
String code = (String) request.getSession().getAttribute("kaptcha");
// 用户输入的验证码和 session 作用域中的都不能为空
if (!ObjectUtils.isEmpty(parameter) && !ObjectUtils.isEmpty(code) && parameter.equalsIgnoreCase(code)) {
return super.attemptAuthentication(request, response);
}
// 没有通过则执行自定义异常
throw new KaptchaNotMatchException("验证码不匹配!");
}
} -
自定义验证码异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15package com.example.exception;
import org.springframework.security.core.AuthenticationException;
public class KaptchaNotMatchException extends AuthenticationException {
public KaptchaNotMatchException(String msg, Throwable cause) {
super(msg, cause);
}
public KaptchaNotMatchException(String msg) {
super(msg);
}
} -
Security
配置类 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124package com.example.config;
import com.example.filter.LoginFilter;
import com.example.service.MyUserDetailsService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationManager;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import java.util.HashMap;
import java.util.Map;
public class SecurityConfigure extends WebSecurityConfigurerAdapter {
// 使用数据库中的用户认证
private final MyUserDetailsService myUserDetailsService;
public SecurityConfigure(final MyUserDetailsService myUserDetailsService) {
this.myUserDetailsService = myUserDetailsService;
}
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService);
}
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
// 自定义 filter 交给工厂管理
public LoginFilter loginFilter() throws Exception {
LoginFilter loginFilter = new LoginFilter();
// 接受指定的 json 用户名 key
loginFilter.setUsernameParameter("username");
// 接受指定的 json 密码 key
loginFilter.setPasswordParameter("password");
// 接受指定的 json 验证码 key
loginFilter.setKaptchaParameter("code");
// 注入 authdicationManager
loginFilter.setAuthenticationManager(authenticationManagerBean());
// 认证成功后
loginFilter.setAuthenticationSuccessHandler((req, resp, auth) -> {
Map<String, Object> result = new HashMap<>();
result.put("msg", "登录成功");
// 状态码: 200
resp.setStatus(HttpStatus.OK.value());
result.put("authentication", auth);
resp.setContentType("application/json;charset=UTF-8");
String json = new ObjectMapper().writeValueAsString(result);
resp.getWriter().println(json);
});
// 认证失败后
loginFilter.setAuthenticationFailureHandler((req, resp, ex) -> {
Map<String, Object> result = new HashMap<>();
result.put("msg", "登录失败: " + ex.getMessage());
// 服务器内部错误 状态码: 500
resp.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
resp.setContentType("application/json;charset=UTF-8");
String json = new ObjectMapper().writeValueAsString(result);
resp.getWriter().println(json);
});
return loginFilter;
}
// 配置安全
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 放行资源
.antMatchers("/captcha/**", "/index", "/login","/code").permitAll()
// 所有请求都需要认证
.anyRequest().authenticated().and().formLogin()
// 使用自定义登录页面
.loginPage("/login")
// 退出
.and()
// 认证异常的处理
.exceptionHandling()
.authenticationEntryPoint((req, resp, ex) -> {
Map<String, Object> result = new HashMap<>();
result.put("msg", "请认证之后再去处理....");
result.put("status", HttpStatus.UNAUTHORIZED.value());
resp.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
String json = new ObjectMapper().writeValueAsString(result);
resp.getWriter().println(json);
})
.and()
.logout().logoutUrl("/logout")
// 退出成功后
.logoutSuccessHandler((req, resp, auth) -> {
Map<String, Object> result = new HashMap<>();
result.put("msg", "注销成功");
result.put("authentication", auth.getPrincipal());
resp.setContentType("application/json;charset=UTF-8");
resp.setStatus(HttpStatus.OK.value());
String json = new ObjectMapper().writeValueAsString(result);
resp.getWriter().println(json);
})
// 禁用跨站
.and().csrf().disable();
// at: 用在某个 filter 替换过滤器链中哪个 filter
// before: 放在过滤器链中那个 filter 之前
// after: 放在过滤器链中那个 filter 之后
http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
}
} -
前端页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录 </title>
</head>
<body>
<form th:action="@{/login}" method="post">
<p>
用户名: <input type="text" name="username"/>
</p>
<p>
密码: <input type="text" name="password"/>
</p>
<p>
验证码: <input type="text" name="code"/><img th:src="@{/code}">
</p>
<input type="submit" value="登录">
</form>
</body>
</html>输入错误的验证码
-
-
前后端分离的验证码认证
-
生成验证码控制器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37package com.example.controller;
import com.google.code.kaptcha.Producer;
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.FastByteArrayOutputStream;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpSession;
import java.awt.image.BufferedImage;
import java.io.IOException;
public class SplitCaptchaController {
private final Producer producer;
public SplitCaptchaController(final Producer producer) {
this.producer = producer;
}
public String getVerifyCode(HttpSession session) throws IOException {
// 生成验证码
String text = producer.createText();
// 存储到 session/redis
session.setAttribute("captcha", text);
// 生成图片
BufferedImage image = producer.createImage(text);
FastByteArrayOutputStream fos = new FastByteArrayOutputStream();
ImageIO.write(image, "jpg", fos);
// 返回 base64
return Base64.encodeBase64String(fos.toByteArray());
}
} -
base64
转图片前缀 1
data:image/jpg;base64,
base64
-
更换 JSON
获取1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74package com.example.filter;
import com.example.exception.KaptchaNotMatchException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.util.ObjectUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
/**
* 自定义前后端认证
*/
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
private static final String FORM_KAPTCHA_KEY = "code";
private String kaptchaParameter = FORM_KAPTCHA_KEY;
public String getKaptchaParameter() {
return this.kaptchaParameter;
}
public void setKaptchaParameter(final String kaptchaParameter) {
this.kaptchaParameter = kaptchaParameter;
}
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
log.error("attemptAuthentication: [{}]", "Override attemptAuthentication...................................");
// 1. 判断请求方式是否是 POST
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
// 2. 判断 数据是否是 JSON 格式
if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
try {
// 将请求体中的数据进行反序列化
Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
// TODO: 先进行验证码校验
// 获取请求中的验证码
String parameter = userInfo.get(getKaptchaParameter());
String username = userInfo.get(getUsernameParameter());
String password = userInfo.get(getPasswordParameter());
// 获取 session 中的验证码
String code = (String) request.getSession().getAttribute("kaptcha");
log.error("username: [{}]", username);
log.error("password: [{}]", password);
log.error("code: [{}]", code);
// 用户输入的验证码和 session 作用域中的都不能为空
if (!ObjectUtils.isEmpty(parameter) && !ObjectUtils.isEmpty(code) && parameter.equalsIgnoreCase(code)) {
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return super.attemptAuthentication(request, response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
throw new KaptchaNotMatchException("验证码不匹配");
}
return super.attemptAuthentication(request, response);
}
}
-
密码加密
-
认识
PasswordEncoder
1
2
3
4
5
6
7
8
9
10public interface PasswordEncoder {
String encode(CharSequence rawPassword);
boolean matches(CharSequence rawPassword, String encodedPassword);
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}encode
: 用来进行铭文加密的matches
: 用来比较密码的方法upgradeEncoding
: 用来给密码进行升级的方法
-
所有未过期的实现类
所有未过期的实现类 -
密码更新
-
实现
Service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48package com.example.service;
import com.example.entity.Role;
import com.example.entity.User;
import com.example.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsPasswordService;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import java.util.List;
public class MyUserDetailsService implements UserDetailsService, UserDetailsPasswordService {
private final UserMapper userMapper;
public MyUserDetailsService(final UserMapper userMapper) {
this.userMapper = userMapper;
}
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 根据用户名查询用户
User user = userMapper.findUserByUserName(username);
// 如果用户为空
if (ObjectUtils.isEmpty(user)) throw new UsernameNotFoundException("用户名错误!");
// 查询权限信息
List<Role> roles = userMapper.getRoleByUid(user.getId());
user.setRoles(roles);
return user;
}
// 实现密码更新
public UserDetails updatePassword(UserDetails user, String newPassword) {
Integer updatePassword = userMapper.updatePassword(user.getUsername(), newPassword);
if (updatePassword == 1) {
((User) user).setPassword(newPassword);
}
return user;
}
} -
UserMapper
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<mapper namespace="com.example.mapper.UserMapper">
<!-- User findUserByUserName(String username); -->
<select id="findUserByUserName" resultType="user">
select *
from user
where username = #{username}
</select>
<!-- List<Role> getRoleByUid(Integer uid); -->
<select id="getRoleByUid" resultType="role">
select r.id, r.name, r.name_zh
from role r,
user_role ur
where r.id = ur.uid
and ur.uid = #{uid}
</select>
<!-- Integer updatePassword(@Param("username") String username,@Param("password") String password);-->
<update id="updatePassword">
update `user`
set password = #{password}
where username = #{username}
</update>
</mapper> -
更新密码是在原密码上直接更新用户密码的加密方式
{noop}(明文) => {bcrypt}
加密方式
-
RemaemberMe
-
配置类中添加
1
2
3
4
5
6
7
8
9
10
11
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and().formLogin()
// 记住我
.and().rememberMe()
.and().csrf().disable();
} -
自定义登录认证
开启后 根据源码分析, 在自定义页面上实现如下即可 -
原理分析
勾选后, 传递参数 remember-me: on
RememberMeAuthenticationFilter
源码分析 1
2// RememberMeAuthenticationFilter 中的核心
Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36// 核心 autologin(会话超时后
(一段时间过期)) 触发 autologin
public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
String rememberMeCookie = this.extractRememberMeCookie(request);
if (rememberMeCookie == null) {
return null;
} else {
this.logger.debug("Remember-me cookie detected");
if (rememberMeCookie.length() == 0) {
this.logger.debug("Cookie was empty");
this.cancelCookie(request, response);
return null;
} else {
try {
String[] cookieTokens = this.decodeCookie(rememberMeCookie);
UserDetails user = this.processAutoLoginCookie(cookieTokens, request, response);
this.userDetailsChecker.check(user);
this.logger.debug("Remember-me cookie accepted");
return this.createSuccessfulAuthentication(request, user);
} catch (CookieTheftException var6) {
this.cancelCookie(request, response);
throw var6;
} catch (UsernameNotFoundException var7) {
this.logger.debug("Remember-me login was valid but corresponding user not found.", var7);
} catch (InvalidCookieException var8) {
this.logger.debug("Invalid remember-me cookie: " + var8.getMessage());
} catch (AccountStatusException var9) {
this.logger.debug("Invalid UserDetails: " + var9.getMessage());
} catch (RememberMeAuthenticationException var10) {
this.logger.debug(var10.getMessage());
}
this.cancelCookie(request, response);
return null;
}
}
}cookie: remember-me
-
总结
当用户通过用户名
/ 密码的形式登录成功后,系统会根据用户的用户名、密码以及令牌的过期时间计算出一个签名,这个签名使用 MD5
消息摘要算法生成,是不可逆的。然后再将用户名,令牌过期时间以及签名拼接成一个字符串,中间用 :
隔开,对拼接好的字符串进行 Base64
编码,然后再将编码后的结果返回到前端,也就是我们在浏览器中看到的令牌。当会话过期后,访问系统资源时会自动携带 cookie
中的令牌,服务端拿到 cookie
中的令牌后,先进行 Base64
解码,如果没有过期,则根据令牌中的用户名和查询出用户信息,接着在计算出一个签名和令牌中的签名进行对比,如果一致,表示会牌是合法令牌,自动登录成功,否则自动登录失败 -
原理图总结
总结 -
配置持久化令牌
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68// 前提是整合: mybatis
package com.example.config;
import com.example.service.MyUserDetailsService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import javax.sql.DataSource;
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final DataSource dataSource;
private final MyUserDetailsService myUserDetailsService;
public SecurityConfig(DataSource dataSource, final MyUserDetailsService myUserDetailsService) {
this.dataSource = dataSource;
this.myUserDetailsService = myUserDetailsService;
}
// 自定义 AuthenticationManager 推荐 并没有在工厂在暴露出来
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
log.error("自定义 AuthenticationManager: " + builder);
builder.userDetailsService(myUserDetailsService);
}
// 作用: 用来将自定义 AuthenticationManager 在工厂中进行暴露 可以在任意位置注入
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated().and().formLogin()
// 记住我
.and()
.rememberMe()
// rememberMe 持久化
.tokenRepository(persistentTokenRepository())
.and().csrf().disable();
}
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
// 第一次启动时创建表结构(true) 第二次: false
jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
}-
错误
错误原因: jdbcTokenRepository.setCreateTableOnStartup(true); 第二次启动时数据库中已经存在表结构
-
创建的表
rememberMe
实现持久化
-
自定义记住我 (前后端分离实现)
-
传统
WEB
-
配置中开启
记住我
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated().and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/login")
// 登录成功后的跳转
.defaultSuccessUrl("/test", true)
.and()
// 记住我
.rememberMe()
// remember 的持久化
.tokenRepository(persistentTokenRepository())
.and().csrf().disable();
} -
checkbox
的值 1
2
3
4
5
6
7
8
9
10
11
12
13
14// 源码
protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
if (this.alwaysRemember) {
return true;
} else {
String paramValue = request.getParameter(parameter);
if (paramValue == null || !paramValue.equalsIgnoreCase("true") && !paramValue.equalsIgnoreCase("on") && !paramValue.equalsIgnoreCase("yes") && !paramValue.equals("1")) {
this.logger.debug(LogMessage.format("Did not send remember-me cookie (principal did not set parameter '%s')", parameter));
return false;
} else {
return true;
}
}
} -
页面定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录 </title>
</head>
<body>
<form th:action="@{/login}" method="post">
<p>
用户名: <input type="text" name="username"/>
</p>
<p>
密码: <input type="text" name="password"/>
</p>
<p>
记住我: <input name="remember-me" type="checkbox" value="true"/>
</p>
<input type="submit" value="登录">
</form>
</body>
</html> -
点击
记住我
登录,生成 cookie
cookie
-
-
前后端分离
-
创建一个前后端分离的项目
-
rememberMeRequested => AbstractRememberMeServices(类中)
1
2
3
4
5
6
7
8
9
10
11
12
13protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
if (this.alwaysRemember) {
return true;
} else {
String paramValue = request.getParameter(parameter);
if (paramValue == null || !paramValue.equalsIgnoreCase("true") && !paramValue.equalsIgnoreCase("on") && !paramValue.equalsIgnoreCase("yes") && !paramValue.equals("1")) {
this.logger.debug(LogMessage.format("Did not send remember-me cookie (principal did not set parameter '%s')", parameter));
return false;
} else {
return true;
}
}
} -
security
配置类 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129package com.example.config;
import com.example.filter.LoginFilter;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationManager;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.InMemoryTokenRepositoryImpl;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class SecurityConfigure extends WebSecurityConfigurerAdapter {
// 使用内存中的用户认证
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("admin").build());
return inMemoryUserDetailsManager;
}
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService());
}
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
// 自定义 filter 交给工厂管理
public LoginFilter loginFilter() throws Exception {
LoginFilter loginFilter = new LoginFilter();
// 指定认证的 url
loginFilter.setFilterProcessesUrl("/doLogin");
// 接受指定的 json 用户名 key
loginFilter.setUsernameParameter("username");
// 接受指定的 json 密码 key
loginFilter.setPasswordParameter("password");
// 注入 authenticationManager
loginFilter.setAuthenticationManager(authenticationManagerBean());
// TODO: 设置认证成功后时使用自定义 rememberMeServices
loginFilter.setRememberMeServices(rememberMeServices());
// 认证成功后
loginFilter.setAuthenticationSuccessHandler((req, resp, auth) -> {
Map<String, Object> result = new HashMap<>();
result.put("msg", "登录成功");
// 状态码: 200
resp.setStatus(HttpStatus.OK.value());
result.put("authentication", auth);
resp.setContentType("application/json;charset=UTF-8");
String json = new ObjectMapper().writeValueAsString(result);
resp.getWriter().println(json);
});
// 认证失败后
loginFilter.setAuthenticationFailureHandler((req, resp, ex) -> {
Map<String, Object> result = new HashMap<>();
result.put("msg", "登录失败: " + ex.getMessage());
// 服务器内部错误 状态码: 500
resp.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
resp.setContentType("application/json;charset=UTF-8");
String json = new ObjectMapper().writeValueAsString(result);
resp.getWriter().println(json);
});
return loginFilter;
}
// 配置安全
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 所有请求都需要认证
.anyRequest().authenticated().and().formLogin()
// TODO: 1. 认证成功保存记住我 cookie 到客户端 2. 只有 cookie 写入到客户端成功才能实现自动登录功能
.and().rememberMe()
// 设置自动登录使用那个 rememberMeServices
.rememberMeServices(rememberMeServices())
// 退出
.and()
.exceptionHandling()
.authenticationEntryPoint((req, resp, ex) -> {
Map<String, Object> result = new HashMap<>();
result.put("msg", "请认证之后再去处理....");
result.put("status", HttpStatus.UNAUTHORIZED.value());
resp.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
String json = new ObjectMapper().writeValueAsString(result);
resp.getWriter().println(json);
})
.and()
.logout().logoutUrl("/logout")
// 退出成功后
.logoutSuccessHandler((req, resp, auth) -> {
Map<String, Object> result = new HashMap<>();
result.put("msg", "注销成功");
result.put("authentication", auth.getPrincipal());
resp.setContentType("application/json;charset=UTF-8");
resp.setStatus(HttpStatus.OK.value());
String json = new ObjectMapper().writeValueAsString(result);
resp.getWriter().println(json);
})
// 禁用跨站
.and().csrf().disable();
// at: 用在某个 filter 替换过滤器链中哪个 filter
// before: 放在过滤器链中那个 filter 之前
// after: 放在过滤器链中那个 filter 之后
http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
}
public RememberMeServices rememberMeServices() {
return new MyPersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(), userDetailsService(), new InMemoryTokenRepositoryImpl());
}
} -
自定义过滤器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59package com.example.filter;
import com.example.config.MyPersistentTokenBasedRememberMeServices;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices;
import org.springframework.util.ObjectUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
import java.util.UUID;
/**
* 自定义前后端认证
*/
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
log.error("attemptAuthentication: [{}]", "Override attemptAuthentication...................................");
// 1. 判断请求方式是否是 POST
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
// 2. 判断 数据是否是 JSON 格式
if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
try {
// 将请求体中的数据进行反序列化
Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
String username = userInfo.get(getUsernameParameter());
String password = userInfo.get(getPasswordParameter());
// 记住我 AbstractRememberMeServices.DEFAULT_PARAMETER = remember-me
String rememberValue = userInfo.get(AbstractRememberMeServices.DEFAULT_PARAMETER);
if (!ObjectUtils.isEmpty(rememberValue)) {
request.setAttribute(AbstractRememberMeServices.DEFAULT_PARAMETER, rememberValue);
}
log.error("username: [{}]", username);
log.error("password: [{}]", password);
log.error("remember: [{}]", rememberValue);
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// 如果不是 JSON 格式数据,则调用传统方式进行认证
return super.attemptAuthentication(request, response);
}
} -
PersistentTokenBasedRememberMeServices
类集成重写 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37package com.example.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.log.LogMessage;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
public class MyPersistentTokenBasedRememberMeServices extends PersistentTokenBasedRememberMeServices {
public MyPersistentTokenBasedRememberMeServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {
super(key, userDetailsService, tokenRepository);
}
/**
* 自定义获取 remember-me 方式
*
* @param request
* @param parameter
* @return
*/
protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
log.info("parameter: [{}]", parameter);
String paramValue = request.getAttribute(parameter).toString();
if (paramValue == null || !paramValue.equalsIgnoreCase("true") && !paramValue.equalsIgnoreCase("on") && !paramValue.equalsIgnoreCase("yes") && !paramValue.equals("1")) {
return false;
} else {
return true;
}
}
} -
源文件
https://gitee.com/coder-itl/spring-security/tree/remember-me
-
测试
配置 session
过期时间 remember-me
-
必须传递
remember-me
?如何优雅处理 remember-me
-
会话
简介
当浏览器调用接口登录成功后,服务端会和浏览器之间建立一个会话session
)SessionId
,SessionId
Session
Session
Session
Spring Security
HttpSession
SessionManagementFilter
Session AutheaticationStrateey
SessionManagomentFiter
session
SessionAuthenticationStrategy
会话并发管理
-
简介
会话并发管理就是指在当前系统中,同一个用户可以同时创建多少个会话,如果一个设备对应一个会话,那么也可以简单理解为同一个用户可以同时在多少台设备上进行登录。默认情况下,同一用户在多少台设备上登录并没有限制,不过开发者可以在
spring security
中对此进行配置 -
会话配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29package com.example.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.session.HttpSessionEventPublisher;
public class SecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.anyRequest().authenticated()
.and().formLogin()
.and().csrf().disable()
// 开启会话管理
.sessionManagement()
// 允许会话最大并发只能一个客户端 设置会话的并发数
.maximumSessions(1)
;
}
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
}-
访问测试
第二个访问会导致第一个访问失效 -
说明
sessionManagement()
用来开启会话管理, maximumSessions
指定会话的并发数 HttpSessionEventPublisher
提供一个 Http SessionEventPublishor
实例, SpringSecurity
中通过一个 Map
集合来维护当前的 HttpSession
记录, 进而实现会话的并发管理。当用户登录成功时, 就像集合中添加一条 Http Session
记录,当会话销毁时,就从集合中移除一条 HttpSession
记录, HttpSessionEventPublisher
实现了 Fttp SessionListener
接口, 可以监听到 HttpSession
的创建和销毁事件,并将 Fltp Session
的创建 / 销毁事件发布出去,这样,当有 HttpSession
销毁时, SpringSecurity
就可以感知到该事件了。
-
传统 WEB 开发处理失效
-
配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33package com.example.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.session.HttpSessionEventPublisher;
public class SecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.anyRequest().authenticated()
.and().formLogin()
.and().csrf().disable()
// 开启会话管理
.sessionManagement()
// 允许会话最大并发只能一个客户端 设置会话的并发数
.maximumSessions(1)
// TODO: 传统 web 失效处理 当用户被挤下线之后跳转路径
.expiredUrl("/login")
;
}
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
}
前后端分离失效处理
-
配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45package com.example.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
public class SecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.anyRequest().authenticated()
.and().formLogin()
.and().csrf().disable()
// 开启会话管理
.sessionManagement()
// 允许会话最大并发只能一个客户端 设置会话的并发数
.maximumSessions(1)
// TODO: 前后端分离
.expiredSessionStrategy(event -> {
HttpServletResponse response = event.getResponse();
response.setContentType("application/json;charset=UTF-8");
Map<String, Object> result = new HashMap<>();
result.put("status", 500);
result.put("msg", "当前会话已经失效,请重新登录!");
String json = new ObjectMapper().writeValueAsString(result);
response.getWriter().println(json);
response.flushBuffer();
})
;
}
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
} -
访问测试
前后分离会话失效处理
禁止再次登录
-
即:
只允许在一台设备登录,
只有登录设备主动退出后, 其他客户端才允许登录 -
配置
1
2
3
4
5
6// 开启会话管理
.sessionManagement()
// 允许会话最大并发只能一个客户端 设置会话的并发数
.maximumSessions(1)
// TODO: 登录之后禁止再次登录
.maxSessionsPreventsLogin(true)只允许一台客户端登录
集群会话共享解决方案
-
配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60package com.example.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.security.SpringSessionBackedSessionRegistry;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final FindByIndexNameSessionRepository findByIndexNameSessionRepository;
public SecurityConfig(FindByIndexNameSessionRepository findByIndexNameSessionRepository) {
this.findByIndexNameSessionRepository = findByIndexNameSessionRepository;
}
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.anyRequest().authenticated()
.and().formLogin()
.and().logout()
.and().csrf().disable()
// 开启会话管理
.sessionManagement()
// 允许会话最大并发只能一个客户端 设置会话的并发数
.maximumSessions(1)
// 登录之后禁止再次登录
.maxSessionsPreventsLogin(true)
// 将 session 交给谁管理
.sessionRegistry(sessionRegistry())
// TODO: 前后端分离
.expiredSessionStrategy(event -> {
HttpServletResponse response = event.getResponse();
response.setContentType("application/json;charset=UTF-8");
Map<String, Object> result = new HashMap<>();
result.put("status", 500);
result.put("msg", "当前会话已经失效,请重新登录!");
String json = new ObjectMapper().writeValueAsString(result);
response.getWriter().println(json);
response.flushBuffer();
})
;
}
// 创建 session 同步到 redis 中方案
public SpringSessionBackedSessionRegistry sessionRegistry() {
return new SpringSessionBackedSessionRegistry(findByIndexNameSessionRepository);
}
} -
创建项目集群
-
拷贝并修改端口
拷贝 (①) 添加 VM
② -
修改端口
③ 修改端口 -Dserver.port=8081
-
-
连接
redis
客户端,查看访问之后生成的 键
信息不同客户端访问测试 (注意端口) 集群会话记录
CSRF 漏洞保护
简介
CSRF(Cross-Site Request Forgery 跨站请求伪造)
,也可以称为一键式攻击 ( one-click-attack
),通常缩写为CSRF/XSRF
CSRF
攻击是一种挟持用户在当前已登录的浏览器上发送恶意请求的攻击方法。相对于 XSS
利用用户对指定网站的信任, CSRF
则是利用网站对用户网页浏览器的信任。简单来说, CSRF
是攻击者通过一些技术手段欺骗用户的浏览器, 去访问一个用户层级认证过的网站并执行恶意请求,例如发送右键、发送消息,甚至财产操作 (如转账和购买商品)。由于客户端 (浏览器) 已经在该网站上认证过,所以该网站会认为是真正用户在操作而执行的请求 (实际上这个并非的本意)
开启 CSRF 防御
-
配置
1
.csrf();
-
查看网页源码
携带 CSRF
令牌
传统 WEB 开发中使用 CSRF
-
传统
web
只需要在配置类中开启 csrf
即可 -
开启
CSRF
防御后会自动在提交的表单中加入如下代码,需要在开启之后手动加入如下代码,并随着请求提交。获取服务端令牌方式如下 1
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
前后分离使用 CSRF
-
前后端分离开发时,
只需要将生成的 csrf
放入到 cookie
中, 并在请求时获取 cookie
中令牌信息进行提交即可。 -
访问
前后端分离下的
,使其产生login cookie
生成 XSRF-TOKEN
的 cookie
认证未通过 -
配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99package com.example.config;
public class SecurityConfigure extends WebSecurityConfigurerAdapter {
// 使用内存中的用户认证
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}root").roles("admin").build());
return inMemoryUserDetailsManager;
}
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService());
}
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
// 自定义 filter 交给工厂管理
public LoginFilter loginFilter() throws Exception {
LoginFilter loginFilter = new LoginFilter();
// 指定认证的 url
loginFilter.setFilterProcessesUrl("/login");
// 接受指定的 json 用户名 key
loginFilter.setUsernameParameter("username");
// 接受指定的 json 密码 key
loginFilter.setPasswordParameter("password");
// 注入 authenticationManager
loginFilter.setAuthenticationManager(authenticationManagerBean());
// 认证成功后
loginFilter.setAuthenticationSuccessHandler((req, resp, auth) -> {
Map<String, Object> result = new HashMap<>();
result.put("msg", "登录成功");
// 状态码: 200
resp.setStatus(HttpStatus.OK.value());
result.put("authentication", auth);
resp.setContentType("application/json;charset=UTF-8");
String json = new ObjectMapper().writeValueAsString(result);
resp.getWriter().println(json);
});
// 认证失败后
loginFilter.setAuthenticationFailureHandler((req, resp, ex) -> {
Map<String, Object> result = new HashMap<>();
result.put("msg", "登录失败: " + ex.getMessage());
// 服务器内部错误 状态码: 500
resp.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
resp.setContentType("application/json;charset=UTF-8");
String json = new ObjectMapper().writeValueAsString(result);
resp.getWriter().println(json);
});
return loginFilter;
}
// 配置安全
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 所有请求都需要认证
.anyRequest().authenticated().and().formLogin()
// 退出
.and()
.exceptionHandling()
.authenticationEntryPoint((req, resp, ex) -> {
Map<String, Object> result = new HashMap<>();
result.put("msg", "请认证之后再去处理....");
result.put("status", HttpStatus.UNAUTHORIZED.value());
resp.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
String json = new ObjectMapper().writeValueAsString(result);
resp.getWriter().println(json);
})
.and()
.logout().logoutUrl("/logout")
// 退出成功后
.logoutSuccessHandler((req, resp, auth) -> {
Map<String, Object> result = new HashMap<>();
result.put("msg", "注销成功");
result.put("authentication", auth.getPrincipal());
resp.setContentType("application/json;charset=UTF-8");
resp.setStatus(HttpStatus.OK.value());
String json = new ObjectMapper().writeValueAsString(result);
resp.getWriter().println(json);
})
// 开启跨站: 将令牌保存到 cookie 中并允许 cookie 被前端获取
.and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
// at: 用在某个 filter 替换过滤器链中哪个 filter
// before: 放在过滤器链中那个 filter 之前
// after: 放在过滤器链中那个 filter 之后
http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
}
} -
控制器
1
2
3
4
5
6
7
8
9
10
11
12
13package com.example.controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
public class IndexController {
public String index() {
return "index ok...";
}
} -
发送请求携带令牌即可
-
请求参数中携带令牌
1
2key: _csrf
value: "xxx" -
请求头中携带令牌
1
2// 前后端分离认证时,初次访问 /login,
会生成一个 cookie, 获取他得值,将该值以键 `X-XSRF-TOKEN', 值为初次访问时生成的 cookie
X-XSRF-TOKEN: value详细过程 疑问: 那如何直接在第一次就可以登录成功!
解决: 在
vue
中使用请求拦截器之后使用 vue-cookie
获取 cookie
并设置到请求头
-
跨域
Spring 跨域解决方案
-
SpringBoot
解决跨域方案 -
@CrossOrigin
-
扩展
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22package com.example.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 全局跨域处理
*/
public class WebMvcConfig implements WebMvcConfigurer {
public void addCorsMappings(CorsRegistry registry) {
// 对那些请求进行跨域处理
registry.addMapping("/**")
.allowCredentials(false)
.allowedHeaders("*")
.allowedMethods("*")
.allowedOrigins("*")
.maxAge(3600);
}
}
-
-
CrosFilter
: 是Spring Web
中提供的一个处理跨域的过滤器, 开发者也可以通过该过滤器处理跨域 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27package com.example.config;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import java.util.Arrays;
public class MyCrosFilter {
FilterRegistrationBean<CorsFilter> cordFilter() {
FilterRegistrationBean<CorsFilter> registrationBean = new FilterRegistrationBean<>();
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
corsConfiguration.setAllowedMethods(Arrays.asList("*"));
corsConfiguration.setAllowedOrigins(Arrays.asList("*"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
registrationBean.setFilter(new CorsFilter(source));
registrationBean.setOrder(-1);
return registrationBean;
}
}注意: 引入
Spring Security
后 Spring/SpringBoot
中的跨域解决方案会失效。这句话读取后影响了后续的一些判断。
SpringSecurity 跨域解决方案
-
配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31package com.example.config;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
public class SecurityConfigure extends WebSecurityConfigurerAdapter {
// 配置安全
protected void configure(HttpSecurity http) throws Exception {
...
// 跨域 TODO: 这种跨域在后续出现失效,在初次出现这个文档时可以使用, 但是当在后续重新写项目的时候采用此种配置时跨域失效了
http.cors().configurationSource(configurationSource());
...
}
CorsConfigurationSource configurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
corsConfiguration.setAllowedMethods(Arrays.asList("*"));
corsConfiguration.setAllowedOrigins(Arrays.asList("*"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}
}解决跨域 【第一次写文档时正常访问】
异常处理
-
异常处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37package com.example.config;
public class SecurityConfig extends WebSecurityConfigurerAdapter {
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}root").roles("ADMIN").build());
inMemoryUserDetailsManager.createUser(User.withUsername("lisi").password("{noop}lisi").roles("USER").build());
return inMemoryUserDetailsManager;
}
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService());
}
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().mvcMatchers("/hello").hasRole("ADMIN").anyRequest().authenticated().and().formLogin().and().csrf().disable();
// 认证异常处理
http.exceptionHandling().authenticationEntryPoint((req, resp, ex) -> {
resp.setStatus(HttpStatus.UNAUTHORIZED.value());
resp.setContentType("application/json;charset=UTF-8");
resp.getWriter().write("尚未认证,请进行认证操作!");
})
// 授权异常处理
.accessDeniedHandler((res, resp, ex) -> {
resp.setStatus(HttpStatus.FORBIDDEN.value());
resp.setContentType("application/json;charset=UTF-8");
resp.getWriter().write("无权访问!");
});
}
}
授权
权限管理策略
- 基于过滤器的权限管理
( FilterSecurityInterceptor
)- 基于过滤器的权限管理主要是用来拦截
HTTP
请求,拦截下来之后,根据 HTTP
请求地址进行权限校验
- 基于过滤器的权限管理主要是用来拦截
- 基于
AOP
的权限管理 ( MethodSecurityInterceptor
)- 基于
AOP
权限管理主要是用来处理方法级别的权限问题。当需要调用某一个方法时,同过 AOP
将操作拦截下来,然后判断用户受否具备相关的权限。
- 基于
授权-URL-权限管理策略
-
配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44package com.example.config;
import org.springframework.context.annotation.Configuration;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 创建内存中的数据源
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
// 角色权限
inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}root").roles("ADMIN", "USER").build());
inMemoryUserDetailsManager.createUser(User.withUsername("lisi").password("{noop}lisi").roles("USER").build());
// 权限
inMemoryUserDetailsManager.createUser(User.withUsername("wangwu").password("{noop}wangwu").authorities("READ_INFO").build());
return inMemoryUserDetailsManager;
}
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService());
}
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 拥有的角色
.mvcMatchers("/admin").hasRole("ADMIN")
.mvcMatchers("/user").hasRole("USER")
// 拥有权限
.mvcMatchers("/info").hasAuthority("READ_INFO")
.anyRequest().authenticated().and().formLogin().and().csrf().disable();
}
}
// mvcMatchers() 匹配多种: /admin /admin/ /admin.html ...
// antMatchers() 只能匹配一种: /admin
// regexMatchers() 支持正则的匹配
基于方法的权限管理
-
基于方法的权限管理主要是通过
AOP
来实现的, Spring Security
中通过 MethodSecurityInterceptor
来提供相关的实现。不同在于 Filter Security Interceptor
只是在请求之前进行前置处理, MethodSecurityInterceptor
除了前置处理之外的还可以进行后置处理。前置处理就是在请求之前判断是否具备相应的权限,后置处理规则是对方法的执行结果进行二次过滤。前置处理和后置处理分别对应了不用的实现类。 -
@EnableGlobalMethodSecurity
注解是用来开启权限注解, 用法如下 1
2
3
4
5
6
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}-
prePostEnabled
:开启Spring Security
提供的四个权限注解 1
2
3
4: 在目标方式执行之后进行权限校验 : 在目标方法执行之后对方法的返回结果进行过滤 : 在目标方法执行之前进行权限校验
: 在目标方法执行之前对方法进行过滤 -
securedEnabled
: 开启Spring Security
提供的 @Secured
注解支持、该注解不支持权限表达式 1
: 访问目标方法必须具有各相应的角色
-
jsr250Enabled
: 开启JSR-250
提供的注解,主要是 1
2
3
4: 拒绝所有访问 : 允许所有访问
: 访问目标方法必须具备相应的角色
这些注解同时也不支持权限表达式这些基于方法的权限管理相关的注解,一般来说只要设置
prePostEnabled = true
就够用了
-
-
测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52package com.example.controller;
import com.example.entity.User;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.access.prepost.PreFilter;
import org.springframework.web.bind.annotation.*;
import java.util.List;
public class AuthorizedController {
// and / or
public String hello1() {
return "hello";
}
// 是否有权限: READ_INFO
public String hello2() {
return "hello READ_INFO";
}
// TODO: 参数必须是认证的用户
public String hello(String name) {
return "hello: " + name;
}
/**
* filterTarget 必须是 集合/ 数组
* 此接口测试需要在浏览器获取 JSESSIONID 的值,通过认证后访问
*/
public void addUsers(
List<User> users
) {
System.out.println("users= " + users);
}
public User getUserById(Integer id) {
return new User(id, "coder-itl");
}
}参数必须是 认证的用户
userId?id=1
-
对结果集处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26package com.example.controller;
import com.example.entity.User;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
public class AuthorizedController {
// 用来对方法的返回值进行过滤
public List<User> getAll() {
List<User> users = new ArrayList<>();
for (int i = 0; i < 10; i++) {
users.add(new User(i, "i: " + i));
}
return users;
}
} -
其他测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49package com.example.controller;
import com.example.entity.User;
import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.security.DenyAll;
import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
public class AuthorizedController {
// 只能判断角色
public User getUserByUserName() {
return new User(99, "coder-itl");
}
// 具有其中一个即可
public User getUserByUsername2(String username) {
return new User(99, username);
}
public String denyAll() {
return "DenyAll";
}
public String permitAll() {
return "permitAll";
}
// 具有其中一个角色即可
public String rolesAllowed() {
return "rolesAllowed";
}
} -
原理分析
授权原理 ConfigAttribute
在 Spring Security
中, 用户请求一个资源 (通常是一个接口或者一个 Java
方法) 需要的角色会被封装成一个 ConfigAttribute
对象,在 ConfigAttribute
中只有一个 getAttribute
方法,该方法返回一个 String
字符串,就是角色的名称。一般来说,角色名称都带有一个 ROLE
前缀,投票器 AccessDecisionVoter
所作的事情,其实就是比较用户所具有各的角色和请求某个资源所需的 ConfigAttribute
之间的关系 AccesDecisionVoter
和 AccessDecisionManager
都有众多的实现类,在 AccessDecisManager
中会挨个遍历 AccessDecisionVoter
,进而决定是否允许用户访问,因而AccessDecisionVoter
和 AccessDecisionManager
两者的关系类似于 AuthenticationProvider
和 ProviderManager
的关系。
授权实战
-
创建数据表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35create table user
(
id int(11) comment '用户ID',
username varchar(32) comment '用户名',
password varchar(255) comment '密码',
enabled tinyint comment '是否启用',
locked tinyint comment '是否锁定'
) comment '用户表';
create table user_role
(
id int(11),
uid int(11) comment '用户ID',
rid int(11) comment '角色ID'
) comment '用户角色关系表';
create table role
(
id int(11) comment '角色ID',
name varchar(32) comment '角色名称(英)',
nameZh varchar(32) comment '角色名称(中)'
) comment '角色表';
create table menu
(
id int(11),
pattern varchar(128)
) comment '菜单表';
create table menu_role
(
id int(11),
mid int(11),
rid int(11)
);逆向关系 -
用户表数据
1
2
3insert into user values (1, 'admin', '{noop}admin', 1, 1);
insert into user values (2, 'user', '{noop}user', 1, 1);
insert into user values (3, 'coder-itl', '{noop}coder-itl', 1, 1); -
角色表数据
1
2
3insert into role values (1,'ROLE_ADMIN','系统管理员');
insert into role values (2,'ROLE_USER','普通用户');
insert into role values (3,'ROLE_GUEST','游客'); -
菜单表数据
1
2
3insert into menu values (1,'/admin/**');
insert into menu values (2,'/user/**');
insert into menu values (3,'/guest/**'); -
用户角色关系表
1
2
3
4insert into user_role values (1,1,1);
insert into user_role values (2,1,2);
insert into user_role values (3,2,2);
insert into user_role values (4,3,3); -
菜单角色数据
1
2
3
4insert into menu_role values (1, 1, 1);
insert into menu_role values (2, 2, 2);
insert into menu_role values (3, 3, 3);
insert into menu_role values (4, 3, 2); -
源文件
JWT
-
前后端权限校验时序图
时序图
OAuth2
简介
-
简介
OAuth2
是一个开放式标准,该标准允许用户让第三方应用访问该用户在某一网站上存储的私密资源( 如: 头像、照片、视频等
),并且在这个过程中无需将用户名和密码提供给第三方应用。通过令牌 ( token
)可以实现这一功能,每一个令牌授权一个特定的网站在特定的时段内允许可特定的资源。 OAtuth
让用户可以授权第三方网站灵活访问他们存储在另外一些资源服务器上的特定信息,而非所有的内容。对于用户而言,我们在互联网应用中最常见的 OAuth
应用就是各种第三方登录,例如 QQ
授权登录、微信授权登录、微博授权登录等
四种授权模式
-
授权码模式
- 授权码模式: 常见的第三方平台登陆都是使用这种模式
- 简化模式: 简化模式是不需要第三方服务端参与,直接在浏览器中向授权服务器申请令牌
( token
),如果网站是纯静态页面,则可以采用这种方式 - 密码模式: 密码模式是用户把用户名
/ 密码直接告诉客户端, 客户端使用这些信息后授权服务器申请令牌。这需要用户对客户端高度信任,例如: 客户端应用和服务提供商就是同一家公司。 - 客户端模式: 客户端模式是指哭护短使用自己的名义而不是用户的名义向服务提供者申请授权。严格来说,客户端模式并不能算作
OAuth
协议解决问题的一种解决方案,但是对于开发者而言,在一些为移动端提供的授权服务器上
-
流程分析
流程 1
2
3
4
5
6A: 用户打开客户端以后,
客户端要求用户给予授权
B: 用户同意给予客户端授权
C: 客户端使用上一步获得的授权,向认证服务器申请令牌
D: 认证服务器对客户端进行认证以后,确认无误,同意发放令牌
E: 客户端使用令牌,向资源服务器申请获取资源
F: 资源服务器确认令牌无误,同意向客户端开放资源 -
四种授权模式·
-
授权码模式
-
Third-part application
: 第三方应用程序,简称 客户端
(client) -
Resource Owner
: 资源所有者,简称 用户
(User) -
User Agent
: 用户代理,是指 浏览器
-
Authorization Server
:认证服务器,即服务端专门用来处理认证的服务器 -
Resource Server
: 资源服务器,即服务端存放用户生成的资源的服务器。他于认证服务器,可以是同一台服务器。也可以是不同的服务器 -
流程图
授权码模式流程图 1
2
3
4
5A: 用户访问第三方应用,
第三方应用通过浏览器导向认证服务器
B: 用户选择是否给予客户端授权
C: 假设用户给与授权,认证服务器将用户导向客户端事先指定的`重定向 URI`同时附上一个授权码
D: 客户端收到授权码,附上早先的`重定向 URI`,向认证服务器申请令牌。这一步是在客户端的后台服务器上完成的,对用户不可见
E: 认证服务器核对授权码和重定向的 URI,确认无误后,向客户端发送访问令牌和更新令牌 使用第三方应用 导向认证服务器 -
核心参数
字段 描述 client_id
授权服务器注册应用的唯一标识 response_type
必须 固定值
在授权码中必须为code
redirect_uri
必须 通过客户端注册的重定向 URL
scope
必须 令牌乐意访问的资源权限 read
只读 all
读写 state
可选 存在原样返回客户端,用来防止 CSRF
跨站攻击
-
-
OAuth2 标准接口
/oauth/authorize
: 授权端点/oauth/token
: 获取令牌端点/oauth/confirm_access
: 用户确认授权提交端点/oauth/error
: 授权服务错误信息端点/oauth/check_token
: 用于资源服务访问的令牌解析端点/oauth/token_key
: 提供共有密钥的端点,如果使用JWT
令牌的话
GITHUB 实战
-
github
创建 OAuth
应用 -
生成
ID
和密钥 -
创建项目
-
添加依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies> -
创建配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18package com.example.security.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
public class SecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
// 使用 oauth2 认证
.oauth2Login();
}
} -
创建控制器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17package com.example.controller;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
public class HelloController {
public DefaultOAuth2User hello() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return (DefaultOAuth2User) authentication.getPrincipal();
}
} -
配置
1
2
3
4
5
6
7
8
9
10
11
12server:
port: 8080
spring:
oauth2:
client:
registration:
github:
client-id: 677f02f9cd2397d6d443
client-secret: 055520c9be8efcd85d8ea089f85f81633a1be92a
# 一定要与重定向回调 URL 一致
redirect-uri: http://localhost:8080/login/oauth2/code/github # /login/oauth2/code/github 固定格式 -
访问测试
授权后获取用户信息 -
原理分析
-