保存在客户端
浏览器为每个站点保存的cookie数最多是20个,而且每个cookie最大是4K
同样的Cookie的内容的字符限制针对不同的Cookie版本也有不同。在Cookie Version 0中,某些特殊的字符,例如:空格,方括号,圆括号,等于号(=),逗号,双引号,斜杠,问号,@符号,冒号,分号都不能作为Cookie的内容
登录,购物车,游戏得分或者服务器应该记住的其他内容
偏好设置,主题或其他设置
记录和分析用户行为
Cookie曾经用于一般的客户端存储。是合法的,因为他们是客户端上存储数据的唯一方法。如今,建议使用现代存储API。Cookie随每个请求一起发送,因此他们可能会降低性能(尤其是对于移动数据连接而言)。客户端存储的现代API是Web存储API
和IndexedDB
HTTP/2.0 200 OK
Content-type: text/html
Set-Cookie: yummy_cookie=choco
Set-Cookie: tasty_cookie=strawberry
随着对服务器的每个新请求,浏览器将使用Cookie头将所有以前存储的Cookie发送给服务器
GET /sample_page.html HTTP/2.0
Host: www.example.org
Cookie: yummy_cookie=choco; tasty_cookie=strawberry
Cookie主要分为三类,会话Cookie
,永久Cookie
和 Cookie的Secure和HttpOnly标记
上面的例子就是会话Cookie,因为没有设置Expires或Max-Age。客户端关闭Cookie就会删除
Set-Cookie:id=a3fWa;Expires=Wed, 21 Oct 2015 07:28:00 GMT;
到设置的时间之后才会过期
安全的cookie需要经过https协议通过加密的方法发送到服务器。即使时安全的,也不应该将敏感信息存储在cookie中,因为他们本质上是不安全的,并且此标志不能提供真正的保护
HttpOnly
设置了HttpOnly为true之后,js脚本将无法读取到cookie信息,有效防止XSS攻击
全称Cross SiteScript,跨站脚本 攻击
//设置多个cookie
response.addHeader("Set-Cookie", "uid=112; Path=/; HttpOnly");
response.addHeader("Set-Cookie", "timeout=30; Path=/test; HttpOnly");
//设置https的cookie
response.addHeader("Set-Cookie", "uid=112; Path=/; Secure; HttpOnly");
cookie返回给客户端
@GetMapping("/change-username")
public String setCookie(HttpServletResponse response) {
// 创建一个 cookie
Cookie cookie = new Cookie("username", "Jovan");
//设置 cookie过期时间
cookie.setMaxAge(7 * 24 * 60 * 60); // expires in 7 days
//添加到 response 中
response.addCookie(cookie);
return "Username is changed!";
}
或者用@CookieValue
@GetMapping("/")
public String readCookie(@CookieValue(value = "username", defaultValue = "Atta") String username) {
return "Hey! My username is " + username;
}
springboot中获取cookie
@GetMapping("/all-cookies")
public String readAllCookies(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
return Arrays.stream(cookies)
.map(c -> c.getName() + "=" + c.getValue()).collect(Collectors.joining(", "));
}
return "No cookies";
}
@GetMapping("/cookieTest")
public String setCookie(HttpServletResponse response) {
// 创建一个 cookie
Cookie cookie = new Cookie("c1", "v1");
//设置 cookie过期时间
cookie.setMaxAge(7 * 24 * 60 * 60); // expires in 7 days
// 作用域
cookie.setDomain("localhost");
cookie.setPath("/cookieTest");
//添加到 response 中
response.addCookie(cookie);
return "cookie1";
}
DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<div id="cookie">div>
<button onclick="checkCookie()">checkCookiebutton>
<button onclick="setCookie('c2', 'v2', 1)">setCookiebutton>
body>
<script>
let cookieValue = document.cookie;
document.querySelector("#cookie").innerText = cookieValue;
function setCookie(cname, cvalue, exdays){
let d = new Date();
d.setTime(d.getTime() + (exdays * 24 * 60 * 60));
let expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + ";" + expires;
}
function getCookie(cname){
let name = cname + "=";
let ca = document.cookie.split(';');
for(let i = 0; i < ca.length; i++) {
let c = ca[i].trim();
if (c.indexOf(name) == 0) {
return c.substring(name.length,c.length);
}
}
return "";
}
function checkCookie(){
let c1 = getCookie("c1");
if (c1 != ""){
alert("欢迎 " + c1 + " 再次访问");
}
}
script>
html>
Domain
和Path
标识定义了cookie的作用域,即Cookie应该发送给哪些URL
Domain
标识指定了哪些主机可以接受Cookie,如果不指定,默认为当前主机(不包含子域名),如果指定了Domain,则一般包含子域名
例如,设置Domain=mozilla.org,则Cookie也包含在子域名中developer.mozilla.org
例如,设置Path=/docs,则一下地址都会匹配
很多时候我们都是通过 SessionID
来实现特定的用户,SessionID
一般会选择存放在 Redis 中
关于这种认证方式更详细的过程如下:
Session
,并将 Session
信息存储起来。SessionID
,写入用户的 Cookie
Cookie
将与每个后续请求一起被发送出去。Cookie
上的 SessionID
与存储在内存中或者数据库中的 Session
信息进行比较,以验证用户的身份,返回给用户客户端响应信息的时候会附带用户当前的状态使用 Session
的时候需要注意下面几个点:
Session
的关键业务一定要确保客户端开启了 Cookie
Session
的过期时间 20min-30minSession-Cookie 方案在单体环境是一个非常好的身份认证方案,但多节点不适合
举个例子:假如我们部署了两份相同的服务 A,B,用户第一次登陆的时候 ,Nginx 通过负载均衡机制将用户请求转发到 A 服务器,此时用户的 Session 信息保存在 A 服务器。结果,用户第二次访问的时候 Nginx 将请求路由到 B 服务器,由于 B 服务器没有保存 用户的 Session 信息,导致用户需要重新进行登陆。
我们应该如何避免上面这种情况的出现呢?
经典面试题
一般是通过 Cookie
来保存 JSESSIONID
,假如你使用了 Cookie
保存 JSESSIONID
的方案的话, 如果客户端禁用了 Cookie
,那么 Session
就无法正常工作
但是,并不是没有 Cookie
之后就不能用 Session
了,比如你可以将 jsessionid
放在请求的 url
里面https://javaguide.cn/;jsessionid=xxx
。这种方案的话可行,但是安全性和用户体验感降低。当然,为了你也可以对 jsessionid
进行一次加密之后再传入后端 使用@MatrixVariable 矩阵变量
@Controller
public class CookieController {
// 禁用cookie后 session怎么用
@RequestMapping("/cookie1")
public String cookie1(HttpServletRequest request, HttpServletResponse response,
@CookieValue(value = "JSESSIONID") String jsessionid){
//把user信息保存进session
request.getSession().setAttribute("user",123);
if (!StringUtils.isEmpty(jsessionid)) {
System.out.println("cookie没有被禁用");
request.setAttribute("cookie2Url", "/cookie2");
} else {
//encodeURL方法为url重写方法,重写后会在原来的基础上追加jsessionid 例: /cookie2;jsessionid=DE8EA4AC8934B2BCB5FC8AC5805BFCAD
request.setAttribute("cookie2Url", response.encodeURL("/cookie2"));
}
return "cookie1";
}
@RequestMapping("/cookie2")
public String cookie2(HttpServletRequest request){
System.out.println("user :" + request.getSession().getAttribute("user"));
return "cookie2";
}
}
cookie1
DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<div th:text="${session.user}">div>
<a th:href="@{${cookie2Url}}">我的cookie2a>
body>
html>
cookie2
DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<div th:text="${session.user}">div>
body>
html>
**CSRF(Cross Site Request Forgery)**一般被翻译为 跨站请求伪造
小壮登录了某网上银行,他来到了网上银行的帖子区,看到一个帖子下面有一个链接写着“科学理财,年盈利率过万”,小壮好奇的点开了这个链接,结果发现自己的账户少了 10000 元。这是这么回事呢?原来黑客在链接中藏了一个请求,这个请求直接利用小壮的身份给银行发送了一个转账请求,也就是通过你的 Cookie 向银行发出请求。
科学理财,年盈利率过万>Copy to clipboardErrorCopied
如果别人通过 Cookie
拿到了 SessionId
后就可以代替你的身份访问系统了
需要注意的是不论是 Cookie
还是 Token
都无法避免 跨站脚本攻击(Cross Site Scripting)XSS
跨站脚本攻击(Cross Site Scripting)缩写为 CSS 但这会与层叠样式表(Cascading Style Sheets,CSS)的缩写混淆。因此,有人将跨站脚本攻击缩写为 XSS
XSS 中攻击者会用各种方式将恶意代码注入到其他用户的页面中。就可以通过脚本盗用信息比如 Cookie
推荐阅读:如何防止 CSRF 攻击?—美团技术团队
https://snailclimb.gitee.io/javaguide/#/docs/system-design/authority-certification/basis-of-authority-certification
保存在服务端
sessionid 是一个会话的 key,浏览器第一次访问服务器会在服务器端生成一个 session,有一个 sessionID 和它对应,并返回给浏览器,这个 sessionID 会被保存在浏览器的会话 cookie 中。tomcat 生成的 sessionID 叫做jsessionid
客户端只保存 sessionID 到 cookie 中,而不会保存 session。session 不会因为浏览器的关闭而删除
早些时候,本地存储使用的是 cookie。但是Web 存储需要更加的安全与快速. 这些数据不会被保存在服务器上,但是这些数据只用于用户请求网站数据上.它也可以存储大量的数据,而不影响网站的性能。数据以 键/值 对
存在, web网页的数据只允许该网页访问使用
Internet Explorer 8+, Firefox, Opera, Chrome, 和 Safari支持Web 存储。注意: Internet Explorer 7 及更早IE版本不支持web 存储
客户端存储数据的两个对象为:
localStorage - 用于长久
保存整个网站的数据,保存的数据没有过期时间,直到手动去除
sessionStorage - 用于临时保存同一窗口(或标签页)的数据,在关闭窗口或标签页
之后将会删除这些数据。
localStorage
只要在相同的协议、相同的主机名、相同的端口下,就能读取/修改到同一份localStorage数据。
sessionStorage
比localStorage
更严苛一点,除了协议、主机名、端口外,还要求在同一窗口(也就是浏览器的标签页)下。
生存期
localStorage
理论上来说是永久有效的,即不主动清空的话就不会消失,即使保存的数据超出了浏览器所规定的大小,也不会把旧数据清空而只会报错。但需要注意的是,在移动设备上的浏览器或各Native App
用到的WebView
里,localStorage
都是不可靠的,可能会因为各种原因(比如说退出App、网络切换、内存不足等原因)被清空。
sessionStorage
的生存期顾名思义,类似于session
,只要关闭浏览器(也包括浏览器的标签页),就会被清空。由于sessionStorage
的生存期太短,因此应用场景很有限,但从另一方面来看,不容易出现异常情况,比较可靠。
数据结构
localstorage为标准的键值对(Key-Value,简称KV)数据类型,简单但也易扩展,只要以某种编码方式把想要存储进localstorage的对象给转化成字符串,就能轻松支持。举点例子:把对象转换成json字符串,就能让存储对象了;把图片转换成DataUrl(base64),就可以存储图片了。另外对于键值对数据类型来说,"键是唯一的"这个特性也是相当重要的,重复以同一个键来赋值的话,会覆盖上次的值
过期时间
很遗憾,localstorage原生是不支持设置过期时间的,想要设置的话,就只能自己来封装一层逻辑来实现:
function set(key,value){
var curtime = new Date().getTime();//获取当前时间
localStorage.setItem(key,JSON.stringify({val:value,time:curtime}));//转换成json字符串序列
}
function get(key,exp)//exp是设置的过期时间
{
var val = localStorage.getItem(key);//获取存储的元素
var dataobj = JSON.parse(val);//解析出json对象
if(new Date().getTime() - dataobj.time > exp)//如果当前时间-减去存储的元素在创建时候设置的时间 > 过期时间
{
console.log("expires");//提示过期
}
else{
console.log("val="+dataobj.val);
}
}
容量限制
目前业界基本上统一为5M,已经比cookies的4K要大很多了,省着点用吧骚年
域名限制
由于浏览器的安全策略,localstorage是无法跨域的,也无法让子域名继承父域名的localstorage数据,这点跟cookies的差别还是蛮大的
异常处理
localstorage在目前的浏览器环境来说,还不是完全稳定的,可能会出现各种各样的bug,一定要考虑好异常处理。我个人认为localstorage只是资源本地化的一种优化手段,不能因为使用localstorage就降低了程序的可用性,那种只是在console里输出点错误信息的异常处理我是绝对反对的。localstorage的异常处理一般用try/catch
来捕获/处理异常
如何测试用户当前浏览器是否支持localstorage
目前普遍的做法是检测window.localStorage
是否存在,但某些浏览器存在bug,虽然"支持"localstorage,但在实际过程中甚至可能出现无法setItem()这样的低级bug。因此我建议,可以通过在try/catch
结构里set/get
一个测试数据有无出现异常来判断该浏览器是否支持localstorage,当然测试完后记得删掉测试数据哦。
在使用 web 存储前,应检查浏览器是否支持 localStorage 和sessionStorage
<div id="result">div>
<script>
if(typeof(Storage)!=="undefined")
{
localStorage.sitename="菜鸟教程";
document.getElementById("result").innerHTML="网站名:" + localStorage.sitename;
}
else
{
document.getElementById("result").innerHTML="对不起,您的浏览器不支持 web 存储。";
}
script>
删除localstorage中的sitename
localStorage.removeItem("sitename");
sessionStorage的方法和localStorage一样
作用域
localStorage
只要在相同的协议、相同的主机名、相同的端口下,就能读取/修改到同一份localStorage数据sessionStorage
比localStorage
更严苛一点,除了协议、主机名、端口外,还要求在同一窗口(也就是浏览器的标签页)下本质是一个过滤器链,有很多过滤器
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
spring:
# Spring Security 配置项,对应 SecurityProperties 配置类
security:
# 配置默认的 InMemoryUserDetailsManager 的用户账号与密码。
user:
name: user # 账号
password: user # 密码
roles: ADMIN # 拥有角色
spring.security
配置项,设置 Spring Security 的配置,对应 SecurityProperties 配置类spring.security.user
配置项,UserDetailsServiceAutoConfiguration 会基于配置的信息创建一个用户 User 在内存中。spring.security.user
配置项,UserDetailsServiceAutoConfiguration 会自动创建一个用户名为 "user"
,密码为 UUID 随机的用户 User 在内存中SecurityProperties 里面
/**
* Default user name.
*/
private String name = "user";
/**
* Password for the default user name.
*/
private String password = UUID.randomUUID().toString();
@RestController
@RequestMapping("/admin")
public class AdminController {
@GetMapping("/demo")
public String demo() {
return "示例返回";
}
}
访问 http://127.0.0.1:8080/admin/demo ,因为没有登录,所以输入用户名user ,密码user
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5oxNXK1d-1649940170691)(img/image-20211215094003288.png)]
Spring Security中的 UserDetailsService 接口,PasswordEncoder接口
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
public interface PasswordEncoder {
String encode(CharSequence var1);
boolean matches(CharSequence var1, String var2);
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
PasswordEncoder p = new BCryptPasswordEncoder();
String s = p.encode("123");
boolean matches = p.matches("123", s); // true
<form action="/login" method="post">
user: <input type="text" name="username" />
pwd: <input type="password" name="password" />
rememberMe: <input type="checkbox" name="remember-me" value="true"/>
<input type="submit" value="登录"/>
form>
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private String url;
public MyAuthenticationSuccessHandler(String url) {
this.url = url;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Authentication authentication) throws IOException, ServletException {
User principal = (User)authentication.getPrincipal();
System.out.println(principal.getUsername());
// 为null
System.out.println(principal.getPassword());
// 权限
System.out.println(principal.getAuthorities());
// forward用于服务器内的,redirect是客户端
// httpServletRequest.getRequestDispatcher(this.url).forward(httpServletRequest, httpServletResponse);
httpServletResponse.sendRedirect(this.url);
}
}
在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常
如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理
如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理
所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
private String url;
public MyAuthenticationFailureHandler(String url) {
this.url = url;
}
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AuthenticationException e) throws IOException, ServletException {
httpServletRequest.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, e);
httpServletRequest.getRequestDispatcher(this.url).forward(httpServletRequest, httpServletResponse);
}
}
访问控制方法
static final String permitAll = "permitAll";
private static final String denyAll = "denyAll";
private static final String anonymous = "anonymous"; // anonymous和permitAll类似
private static final String authenticated = "authenticated";
private static final String fullyAuthenticated = "fullyAuthenticated";
private static final String rememberMe = "rememberMe";// 记住我
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gu8SPDfe-1649940170692)(SpringSecurity.assets/image-20211214144425527.png)]
认证
处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException
负责权限校验的过滤器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AcAXIlmi-1649940170692)(SpringSecurity.assets/image-20220407110157269.png)]
自定义UserDetailsService,数据库中验证用户
登录成功后把用户信息存入redis
自定义jwt认证过滤器
总结起来就是不同的用户可以使用不同的功能。这就是权限系统要去实现的效果。
我们不能只依赖前端去判断用户的权限来选择显示哪些菜单哪些按钮。因为如果只是这样,如果有人知道了对应功能的接口地址就可以不通过前端,直接去发送请求来实现相关功能操作。后端必须判断当前用户是否有相应的权限,必须具有所需权限才能进行相应的操作
在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。
所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication。
然后设置我们的资源所需要的权限即可。
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.
// 使用内存中的 InMemoryUserDetailsManager
inMemoryAuthentication()
// 不使用 PasswordEncoder 密码编码器
.passwordEncoder(NoOpPasswordEncoder.getInstance())
// 配置 admin 用户
.withUser("admin").password("admin").roles("ADMIN")
// 配置 normal 用户
.and().withUser("normal").password("normal").roles("NORMAL");
}
}
处,调用AuthenticationManagerBuilder#inMemoryAuthentication()方法,使用内存级别的InMemoryUserDetailsManager
Bean 对象,提供认证的用户信息。
Spring 内置了两种UserDetailsManager
实现:
实际项目中,我们更多采用调用 AuthenticationManagerBuilder#userDetailsService(userDetailsService)
方法,使用自定义实现的 UserDetailsService 实现类,更加灵活且自由的实现认证的用户信息的读取
处,调用AbstractDaoAuthenticationConfigurer#passwordEncoder(passwordEncoder)方法,设置 PasswordEncoder 密码编码器。
处,配置了「admin/admin」和「normal/normal」两个用户,分别对应 ADMIN 和 NORMAL 角色
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 配置请求地址的权限
.authorizeRequests()
.antMatchers("/test/echo").permitAll() // 所有用户可访问
.antMatchers("/test/admin").hasRole("ADMIN") // 需要 ADMIN 角色
.antMatchers("/test/normal").access("hasRole('ROLE_NORMAL')") // 需要 NORMAL 角色。
// 任何请求,访问的用户都需要经过认证
.anyRequest().authenticated()
.and()
// 设置 Form 表单登录
.formLogin()
// .loginPage("/login") // 登录 URL 地址
.permitAll() // 所有用户可访问
.and()
// 配置退出相关
.logout()
// .logoutUrl("/logout") // 退出 URL 地址
.permitAll(); // 所有用户可访问
}
处,调用 HttpSecurity#authorizeRequests()
方法,开始配置 URL 的权限控制。注意看艿艿配置的四个权限控制的配置。下面,是配置权限控制会使用到的方法:
#(String... antPatterns)
方法,配置匹配的 URL 地址,基于 Ant 风格路径表达式 ,可传入多个。#permitAll()
方法,所有用户可访问。#denyAll()
方法,所有用户不可访问。#authenticated()
方法,登录用户可访问。#anonymous()
方法,无需登录,即匿名用户可访问。#rememberMe()
方法,通过 remember me 登录的用户可访问。#fullyAuthenticated()
方法,非 remember me 登录的用户可访问。#hasIpAddress(String ipaddressExpression)
方法,来自指定 IP 表达式的用户可访问。#hasRole(String role)
方法, 拥有指定角色的用户可访问。#hasAnyRole(String... roles)
方法,拥有指定任一角色的用户可访问。#hasAuthority(String authority)
方法,拥有指定权限(authority
)的用户可访问。#hasAuthority(String... authorities)
方法,拥有指定任一权限(authority
)的用户可访问。#access(String attribute)
方法,当 Spring EL 表达式的执行结果为 true
时,可以访问。
处,调用 HttpSecurity#formLogin()
方法,设置 Form 表单登录。
#loginPage(String loginPage)
方法,来进行设置。
处,调用HttpSecurity#logout()方法,配置退出相关。
#logoutUrl(String logoutUrl)
方法,来进行设置。使用默认的退出界面,所以不进行设置。开启对 Spring Security 注解的方法,进行权限验证
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
@RestController
@RequestMapping("/demo")
public class DemoController {
@PermitAll
@GetMapping("/echo")
public String demo() {
return "示例返回";
}
@GetMapping("/home")
public String home() {
return "我是首页";
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/admin")
public String admin() {
return "我是管理员";
}
@PreAuthorize("hasRole('ROLE_NORMAL')")
@GetMapping("/normal")
public String normal() {
return "我是普通用户";
}
}
@PermitAll
注解,等价于 #permitAll()
方法,所有用户可访问。
配置了
.anyRequest().authenticated()
,任何请求,访问的用户都需要经过认证。所以这里@PermitAll
注解实际是不生效的也就是说,Java Config 配置的权限,和注解配置的权限,两者是叠加的
@PreAuthorize
注解,等价于 #access(String attribute)
方法,,当 Spring EL 表达式的执行结果为 true 时,可以访问。
Spring Security 还有其它注解,不过不太常用,可见《区别: @Secured(), @PreAuthorize() 及 @RolesAllowed()》文章。
胖友可以按照如上的说明,进行各种测试。例如说,登录「normal/normal」用户后,去访问 /test/admin
接口,会返回 403 界面,无权限~
@Secured
@EnableGlobalMethodSecurity(securedEnabled = true)
@Secured("ROLE_abc")
@RequestMapping("/toMain")
public String toMain() {
return "main";
}
RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型
简单的rbac例子
CREATE DATABASE /*!32312 IF NOT EXISTS*/`sg_security` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;
USE `sg_security`;
/*Table structure for table `sys_menu` */
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`menu_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '菜单名',
`path` varchar(200) DEFAULT NULL COMMENT '路由地址',
`component` varchar(255) DEFAULT NULL COMMENT '组件路径',
`visible` char(1) DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',
`status` char(1) DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
`perms` varchar(100) DEFAULT NULL COMMENT '权限标识',
`icon` varchar(100) DEFAULT '#' COMMENT '菜单图标',
`create_by` bigint(20) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`update_by` bigint(20) DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`del_flag` int(11) DEFAULT '0' COMMENT '是否删除(0未删除 1已删除)',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='菜单表';
/*Table structure for table `sys_role` */
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(128) DEFAULT NULL,
`role_key` varchar(100) DEFAULT NULL COMMENT '角色权限字符串',
`status` char(1) DEFAULT '0' COMMENT '角色状态(0正常 1停用)',
`del_flag` int(1) DEFAULT '0' COMMENT 'del_flag',
`create_by` bigint(200) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`update_by` bigint(200) DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='角色表';
/*Table structure for table `sys_role_menu` */
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu` (
`role_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
`menu_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '菜单id',
PRIMARY KEY (`role_id`,`menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
/*Table structure for table `sys_user` */
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',
`nick_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称',
`password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',
`status` char(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)',
`email` varchar(64) DEFAULT NULL COMMENT '邮箱',
`phonenumber` varchar(32) DEFAULT NULL COMMENT '手机号',
`sex` char(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',
`avatar` varchar(128) DEFAULT NULL COMMENT '头像',
`user_type` char(1) NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建人的用户id',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`del_flag` int(11) DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
/*Table structure for table `sys_user_role` */
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`user_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '用户id',
`role_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '角色id',
PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
SELECT
DISTINCT m.`perms`
FROM
sys_user_role ur
LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`
LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`
LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`
WHERE
user_id = 2
AND r.`status` = 0
AND m.`status` = 0
我们也可以定义自己的权限校验方法,在@PreAuthorize注解中使用我们的方法。
@Component("ex")
public class SGExpressionRoot {
public boolean hasAuthority(String authority){
//获取当前用户的权限
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
List<String> permissions = loginUser.getPermissions();
//判断用户权限集合中是否存在authority
return permissions.contains(authority);
}
}
在SPEL表达式中使用 @ex相当于获取容器中bean的名字未ex的对象。然后再调用这个对象的hasAuthority方法
@RequestMapping("/hello")
@PreAuthorize("@ex.hasAuthority('system:dept:list')")
public String hello(){
return "hello";
}
.antMatchers("/testCors").hasAuthority("system:dept:list222")
实际上在UsernamePasswordAuthenticationFilter进行登录认证的时候,如果登录成功了是会调用AuthenticationSuccessHandler的方法进行认证成功后的处理的。AuthenticationSuccessHandler就是登录成功处理器。
我们也可以自己去自定义成功处理器进行成功后的相应处理。
@Component
public class SGSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println("认证成功了");
}
}
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationSuccessHandler successHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().successHandler(successHandler);
http.authorizeRequests().anyRequest().authenticated();
}
}
实际上在UsernamePasswordAuthenticationFilter进行登录认证的时候,如果认证失败了是会调用AuthenticationFailureHandler的方法进行认证失败后的处理的。AuthenticationFailureHandler就是登录失败处理器。
我们也可以自己去自定义失败处理器进行失败后的相应处理。
@Component
public class SGFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
System.out.println("认证失败了");
}
}
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationSuccessHandler successHandler;
@Autowired
private AuthenticationFailureHandler failureHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
// 配置认证成功处理器
.successHandler(successHandler)
// 配置认证失败处理器
.failureHandler(failureHandler);
http.authorizeRequests().anyRequest().authenticated();
}
}
@Component
public class SGLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println("注销成功");
}
}
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationSuccessHandler successHandler;
@Autowired
private AuthenticationFailureHandler failureHandler;
@Autowired
private LogoutSuccessHandler logoutSuccessHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
// 配置认证成功处理器
.successHandler(successHandler)
// 配置认证失败处理器
.failureHandler(failureHandler);
http.logout()
//配置注销成功处理器
.logoutSuccessHandler(logoutSuccessHandler);
http.authorizeRequests().anyRequest().authenticated();
}
}
跨站请求伪造,通过伪造用户请求访问受信任站点的非法请求访问,sessionid可能会被第三方恶意劫持,通过这个sessionid向服务器发送请求
spring security默认开启CSRF, 要携带key为,_csrf,值为token
CSRF是指跨站请求伪造(Cross-site request forgery),是web常见的攻击之一。
https://blog.csdn.net/freeking101/article/details/86537087
SpringSecurity去防止CSRF攻击的方式就是通过csrf_token。后端会生成一个csrf_token,前端发起请求的时候需要携带这个csrf_token,后端会有过滤器进行校验,如果没有携带或者是伪造的就不允许访问。
我们可以发现CSRF攻击依靠的是cookie中所携带的认证信息。但是在前后端分离的项目中我们的认证信息其实是token,而token并不是存储中cookie中,并且需要前端代码去把token设置到请求头中才可以,所以CSRF攻击也就不用担心了。
HTTP Basic Auth简单点说,就是每次请求api时提供用户的username和password,时配合RESTful API使用的最简单的认证方式。尽量避免使用
使用cookie和session
具体参考OAhth2,代码spring-cloud-demo
下面详细介绍
JWT (json web token)本质上就一段带签名的 JSON 格式的数据,是一个开放的行业标准RFC 7519
JWT 由 3 部分构成:
Header(头部) : 描述 JWT 的元数据,定义了生成签名的算法以及 Token
的类型
{
"alg": "HS256",
"typ": "JWT"
}
对头部的json字符串进行BASE64编码
Payload(载荷) : 用来存放实际需要传递的数据
标准中注册的声明
https://datatracker.ietf.org/doc/html/draft-ietf-oauth-json-web-token-32
iss:jwt签发者
sub:jwt所面向的用户
aud:接收jwt的一方
exp:jwt的过期时间,这个过期时间必须大于签发时间
nbf:定义在什么时间之前,该jwt都是不可用的
iat:jwt的签发时间
jti:jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rCmfh1oa-1649940170693)(SpringSecurity.assets/image-20220402154915686.png)]
Signature(签名) :服务器通过Payload
、Header
和一个密钥(secret
)使用 Header
里面指定的签名算法(默认是 HMAC SHA256)也可以是RSA 生成
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EpCUDnLu-1649940170693)(SpringSecurity.assets/image-20220402155747321.png)]
用.
分隔三个部分
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9UsAP24r-1649940170693)(SpringSecurity.assets/2f01f181f1c4943982e4a0dafda07bd3.png)]
服务器应用在接受到JWT后,会首先对头部和载荷的内容用同一算法再次签名。那么服务器应用是怎么知道我们用的是哪一种算法呢?别忘了,我们在JWT的头部中已经用alg
字段指明了我们的加密算法了。
如果服务器应用对头部和载荷再次以同样方法签名之后发现,自己计算出来的签名和接受到的签名不一样,那么就说明这个Token的内容被别人动过的,我们应该拒绝这个Token,返回一个HTTP 401 Unauthorized响应
所以,在JWT中,不应该在载荷里面加入任何敏感的数据。在上面的例子中,我们传输的是用户的User ID。这个值实际上不是什么敏感内容,一般情况下被知道也是安全的
但是像密码这样的内容就不能被放在JWT中了。如果将用户的密码放在了JWT中,那么怀有恶意的第三方通过Base64解码就能很快地知道你的密码了
在基于 Token 进行身份验证的的应用程序中,服务器通过Payload
、Header
和一个密钥(secret
)创建令牌(Token
)并将 Token
发送给客户端,客户端将 Token
保存在 Cookie 或者 localStorage 里面,以后客户端发出的所有请求都会携带这个令牌。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP Header 的Authorization 字段中 Authorization: Bearer Token
基于Token的身份验证是无状态的,我们不将用户信息存在服务器或Session中
这种概念解决了在服务端存储信息时的许多问题
NoSession意味着你的程序可以根据需要去增减机器,而不用去担心用户是否登录
基于Token的身份验证的过程如下
每一次请求都需要token。token应该在HTTP的头部发送从而保证了Http请求无状态。我们同样通过设置服务器属性Access-Control-Allow-Origin:*,让服务器能接受到来自所有域的请求。
注意:在ACAO头部标明(designating)*时,不得带有像HTTP认证,客户端SSL证书和cookies的证书
当我们在程序中认证了信息并取得token之后,能通过这个Token做许多的事情。甚至能基于创建一个基于权限的token传给第三方应用程序,这些第三方程序能够获取到我们的数据(当然只有在我们允许的特定的token)
token 自身包含了身份验证所需要的所有信息,使得我们的服务器不需要存储 Session 信息,这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。但是,也正是由于 token 的无状态,也导致了它最大的缺点:当后端在token 有效期内废弃一个 token 或者更改它的权限的话,不会立即生效,一般需要等到有效期过后才可以。另外,当用户 Logout 的话,token 也还有效。除非,我们在后端增加额外的处理逻辑。
CSRF(Cross Site Request Forgery) 一般被翻译为 跨站请求伪造,属于网络攻击领域范围。相比于 SQL 脚本注入、XSS等安全攻击方式,CSRF 的知名度并没有它们高。但是,它的确是每个系统都要考虑的安全隐患,就连技术帝国 Google 的 Gmail 在早些年也被曝出过存在 CSRF 漏洞,这给 Gmail 的用户造成了很大的损失。
那么究竟什么是 跨站请求伪造 呢?说简单用你的身份去发送一些对你不友好的请求。举个简单的例子:
小壮登录了某网上银行,他来到了网上银行的帖子区,看到一个帖子下面有一个链接写着“科学理财,年盈利率过万”,小壮好奇的点开了这个链接,结果发现自己的账户少了10000元。这是这么回事呢?原来黑客在链接中藏了一个请求,这个请求直接利用小壮的身份给银行发送了一个转账请求,也就是通过你的 Cookie 向银行发出请求。
<a src="http://www.mybank.com/Transfer?bankId=11&money=10000">科学理财,年盈利率过万a>
导致这个问题很大的原因就是: Session 认证中 Cookie 中的 session_id 是由浏览器发送到服务端的,借助这个特性,攻击者就可以通过让用户误点攻击链接,达到攻击效果。
那为什么 token 不会存在这种问题呢?
我是这样理解的:一般情况下我们使用 JWT 的话,在我们登录成功获得 token 之后,一般会选择存放在 local storage 中。然后我们在前端通过某些方式会给每个发到后端的请求加上这个 token,这样就不会出现 CSRF 漏洞的问题。因为,即使你点击了非法链接发送了请求到服务端,这个非法请求是不会携带 token 的,所以这个请求将是非法的。
但是这样会存在 XSS 攻击中被盗的风险,为了避免 XSS 攻击,你可以选择将 token 存储在标记为httpOnly
的cookie 中。但是,这样又导致了你必须自己提供CSRF保护。
具体采用上面哪种方式存储 token 呢,大部分情况下存放在 local storage 下都是最好的选择,某些情况下可能需要存放在标记为httpOnly
的cookie 中会更好。
使用 Session 进行身份认证的话,需要保存一份信息在服务器端,而且这种方式会依赖到 Cookie(需要 Cookie 保存 SessionId),所以不适合移动端。
但是,使用 token 进行身份认证就不会存在这种问题,因为只要 token 可以被客户端存储就能够使用,而且 token 还可以跨语言使用
使用 Session 进行身份认证的话,实现单点登录,需要我们把用户的 Session 信息保存在一台电脑上,并且还会遇到常见的 Cookie 跨域的问题。但是,使用 token 进行认证的话, token 被保存在客户端,不会存在这些问题
负载可以包含用户一些信息,避免多次查数据库
jwt跨语言
与之类似的具体相关场景有:
这个问题不存在于 Session 认证方式中,因为在 Session 认证方式中,遇到这种情况的话服务端删除对应的 Session 记录即可。但是,使用 token 认证的方式就不好解决了。我们也说过了,token 一旦派发出去,如果后端不增加其他逻辑的话,它在失效之前都是有效的。那么,我们如何解决这个问题呢?查阅了很多资料,总结了下面几种方案:
对于修改密码后 token 还有效问题的解决还是比较容易的,说一种我觉得比较好的方式:使用用户的密码的哈希值对 token 进行签名。因此,如果密码更改,则任何先前的令牌将自动无法验证。
token 有效期一般都建议设置的不太长,那么 token 过期后如何认证,如何实现动态刷新 token,避免用户经常需要重新登录?
我们先来看看在 Session 认证中一般的做法:假如 session 的有效期30分钟,如果 30 分钟内用户有访问,就把 session 有效期延长30分钟。
优点
缺点
https://www.iocoder.cn/Fight/Thoroughly-understand-cookies,-sessions,-tokens/?self
session sticky , 就是让小F的请求一直粘连在机器A上, 但是这也不管用, 要是机器A挂掉了, 还得转到机器B去。
那只好做session 的复制了, 把session id 在两个机器之间搬来搬去, 快累死了。
后来有个叫Memcached的支了招:把session id 集中存储到一个地方, 所有的机器都来访问这个地方的数据, 这样一来,就不用复制了, 但是增加了单点失败的可能性, 要是那个负责session 的机器挂了, 所有人都得重新登录一遍, 估计得被人骂死。
比如说, 小F已经登录了系统, 我给他发一个令牌(token), 里边包含了小F的 user id, 下一次小F 再次通过Http 请求访问我的时候, 把这个token 通过Http header 带过来不就可以了。
不过这和session id没有本质区别啊, 任何人都可以可以伪造, 所以我得想点儿办法, 让别人伪造不了。
那就对数据做一个签名吧, 比如说我用HMAC-SHA256 算法,加上一个只有我才知道的密钥, 对数据做一个签名, 把这个签名和数据一起作为token , 由于密钥别人不知道, 就无法伪造token了。
这个token 我不保存, 当小F把这个token 给我发过来的时候,我再用同样的HMAC-SHA256 算法和同样的密钥,对数据再计算一次签名, 和token 中的签名做个比较, 如果相同, 我就知道小F已经登录过了,并且可以直接取到小F的user id , 如果不相同, 数据部分肯定被人篡改过, 我就告诉发送者:对不起,没有认证。
Token 中的数据是明文保存的(虽然我会用Base64做下编码, 但那不是加密), 还是可以被别人看到的, 所以我不能在其中保存像密码这样的敏感信息。
当然, 如果一个人的token 被别人偷走了, 那我也没办法, 我也会认为小偷就是合法用户, 这其实和一个人的session id 被别人偷走是一样的
这样一来, 我就不保存session id 了, 我只是生成token , 然后验证token , 我用我的CPU计算时间获取了我的 session 存储空间
解除了session id这个负担, 可以说是无事一身轻, 我的机器集群现在可以轻松地做水平扩展, 用户访问量增大, 直接加机器就行。这种无状态的感觉实在是太好了!
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.1version>
dependency>
@Test
public void testCreateToken() {
// 创建JwtBuilder对象
JwtBuilder jwtBuilder = Jwts.builder()
// 声明的标识 "jti":"8888"
.setId("8888")
// 用户 "sub":"Rose"
.setSubject("Rose")
// 签发时间"ita":"xx"
.setIssuedAt(new Date())
// 算法 和 secret
.signWith(SignatureAlgorithm.HS256, "xxxx");
// 获取token
String token = jwtBuilder.compact();
System.out.println(token);
String[] split = token.split("\\.");
System.out.println(Base64Codec.BASE64.decodeToString(split[0]));
System.out.println(Base64Codec.BASE64.decodeToString(split[1]));
// 签名是无法解密的
System.out.println(Base64Codec.BASE64.decodeToString(split[2]));
}
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODg4Iiwic3ViIjoiUm9zZSIsImlhdCI6MTY0ODg4NzI3OH0.Jr3fwC83UDM6ulzDujPBYP_H5yKpXvTWsvC-3mF6SEc
{"alg":"HS256"}
{"jti":"8888","sub":"Rose","iat":164888727
&���/7P3:�\ú3�`��ȪW�5��-���
每次运行都会不一样,因为有签发时间
/**
* 解析token
*/
@Test
public void testParseToken() {
String token = "eyJhbGciOiJIUzI1NiJ9" +
".eyJqdGkiOiI4ODg4Iiwic3ViIjoiUm9zZSIsImlhdCI6MTY0ODg4NzcwNH0" +
".-JnlSmewOw9OPPeG09CA_5EoAmb6BVS3BrQ3-JPtGak";
// 解析token 获取负载中声明的对象
Claims claims = Jwts.parser()
.setSigningKey("xxxx")
.parseClaimsJws(token)
.getBody();
System.out.println(claims.getId());
System.out.println(claims.getSubject());
System.out.println(claims.getIssuedAt());
}
/**
* 创建token,失效时间
*/
@Test
public void testCreateTokenHasExp() {
long now = System.currentTimeMillis();
long exp = now + 60 * 1000;
// 创建JwtBuilder对象
JwtBuilder jwtBuilder = Jwts.builder()
// 声明的标识 "jti":"8888"
.setId("8888")
// 用户 "sub":"Rose"
.setSubject("Rose")
// 签发时间"iat":"xx"
.setIssuedAt(new Date())
// 算法 和 secret
.signWith(SignatureAlgorithm.HS256, "xxxx")
// 过期时间
.setExpiration(new Date(exp));
// 获取token
String token = jwtBuilder.compact();
System.out.println(token);
String[] split = token.split("\\.");
System.out.println(Base64Codec.BASE64.decodeToString(split[0]));
System.out.println(Base64Codec.BASE64.decodeToString(split[1]));
// 签名是无法解密的
System.out.println(Base64Codec.BASE64.decodeToString(split[2]));
}
/**
* 解析token,失效时间
*/
@Test
public void testParseTokenHasExp() {
String token = "eyJhbGciOiJIUzI1NiJ9" +
".eyJqdGkiOiI4ODg4Iiwic3ViIjoiUm9zZSIsImlhdCI6MTY0ODg4ODM2MSwiZXhwIjoxNjQ4ODg4NDIxfQ" +
".npD0NaSm5odMF6bLizqKpR1ascKilhC86sVeERomlrc";
// 解析token 获取负载中声明的对象
Claims claims = Jwts.parser()
.setSigningKey("xxxx")
.parseClaimsJws(token)
.getBody();
System.out.println(claims.getId());
System.out.println(claims.getSubject());
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(simpleDateFormat.format(claims.getIssuedAt()));
System.out.println(simpleDateFormat.format(claims.getExpiration()));
}
// 失效之后会报错io.jsonwebtoken.ExpiredJwtException
/**
* 创建token,自定义声明
*/
@Test
public void testCreateTokenCustom() {
long now = System.currentTimeMillis();
long exp = now + 60 * 1000;
// 创建JwtBuilder对象
JwtBuilder jwtBuilder = Jwts.builder()
// 声明的标识 "jti":"8888"
.setId("8888")
// 用户 "sub":"Rose"
.setSubject("Rose")
// 签发时间"iat":"xx"
.setIssuedAt(new Date())
// 算法 和 secret
.signWith(SignatureAlgorithm.HS256, "xxxx")
// 过期时间
.setExpiration(new Date(exp))
// 自定义声明
.claim("logo", "x.jpg");
// .addClaims(); // 传入map
// 获取token
String token = jwtBuilder.compact();
System.out.println(token);
String[] split = token.split("\\.");
System.out.println(Base64Codec.BASE64.decodeToString(split[0]));
System.out.println(Base64Codec.BASE64.decodeToString(split[1]));
// 签名是无法解密的
System.out.println(Base64Codec.BASE64.decodeToString(split[2]));
}
/**
* 解析token,自定义声明
*/
@Test
public void testParseTokenCustom() {
String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODg4Iiwic3ViIjoiUm9zZSIsImlhdCI6MTY0ODg4ODgyNCwiZXhwIjoxNjQ4ODg4ODg0LCJsb2dvIjoieC5qcGcifQ.UXtvf0m3DliNN4vLIYPTqFrQtCT3tdJE0bE9TU8E_sM";
// 解析token 获取负载中声明的对象
Claims claims = Jwts.parser()
.setSigningKey("xxxx")
.parseClaimsJws(token)
.getBody();
System.out.println(claims.getId());
System.out.println(claims.getSubject());
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(simpleDateFormat.format(claims.getIssuedAt()));
System.out.println(simpleDateFormat.format(claims.getExpiration()));
// 获取自定义声明
System.out.println(claims.get("logo"));
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JrATaJBo-1649940170694)(SpringSecurity.assets/image-20220405005005151.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8MPcpMEj-1649940170694)(SpringSecurity.assets/image-20220329232330005.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gugLeyU7-1649940170694)(SpringSecurity.assets/image-20220329232759051.png)]
参考:https://www.iocoder.cn/Fight/ruanyifeng-oauth_2_0/?self
OAuth 是一个行业的标准授权协议,主要用来授权第三方应用获取有限的权限。而 OAuth 2.0 是对 OAuth 1.0 的完全重新设计,OAuth 2.0 更快,更容易实现,OAuth 1.0 已经被废弃。详情请见:rfc6749。
实际上它就是一种授权机制,它的最终目的是为第三方应用颁发一个有时效性的令牌 Token,使得第三方应用能够通过该令牌获取相关的资源
OAuth 2.0 比较常用的场景就是第三方登录,当你的网站接入了第三方登录的时候一般就是使用的 OAuth 2.0 协议。另外,现在 OAuth 2.0 也常见于支付场景(微信支付、支付宝支付)和开发平台(微信开放平台、阿里开放平台等等)
有一个”云冲印”的网站,可以将用户储存在Google的照片,冲印出来。用户为了使用该服务,必须让”云冲印”读取自己储存在Google上的照片。问题是只有得到用户的授权,Google才会同意”云冲印”读取这些照片。那么,”云冲印”怎样获得用户的授权呢?传统方法是,用户将自己的Google用户名和密码,告诉”云冲印”,后者就可以读取用户的照片了。这样的做法有以下几个严重的缺点。
(1)”云冲印”为了后续的服务,会保存用户的密码,这样很不安全
(2)Google不得不部署密码登录,而我们知道,单纯的密码登录并不安全
(3)”云冲印”拥有了获取用户储存在Google所有资料的权力,用户没法限制”云冲印”获得授权的范围和有效期
(4)用户只有修改密码,才能收回赋予”云冲印”的权力。但是这样做,会使得其他所有获得用户授权的第三方应用程序全部失效
(5)只要有一个第三方应用程序被破解,就会导致用户密码泄漏,以及所有被密码保护的数据泄漏
(1) Third-party application:第三方应用程序,本文中又称”客户端”(client),即上一节例子中的”云冲印”。
(2)HTTP service:HTTP服务提供商,本文中简称”服务提供商”,即上一节例子中的Google。
(3)Resource Owner:资源所有者,本文中又称”用户”(user)。
(4)User Agent:用户代理,本文中就是指浏览器。
(5)Authorization server:认证服务器,即服务提供商专门用来处理认证的服务器。
(6)Resource server:资源服务器,即服务提供商存放用户生成的资源的服务器。它与认证服务器,可以是同一台服务器,也可以是不同的服务器。
OAuth在”客户端”与”服务提供商”之间,设置了一个授权层(authorization layer)。”客户端”不能直接登录”服务提供商”,只能登录授权层,以此将用户与客户端区分开来。”客户端”登录授权层所用的令牌(token),与用户的密码不同。用户可以在登录的时候,指定授权层令牌的权限范围和有效期。“客户端”登录授权层以后,”服务提供商”根据令牌的权限范围和有效期,向”客户端”开放用户储存的资料
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I6EQr3VX-1649940170694)(SpringSecurity.assets/image-20220329234426643.png)]
OAuth 2.0的运行流程如下图,摘自RFC 6749
(A)用户打开客户端以后,客户端要求用户给予授权
(B)用户同意给予客户端授权
(C)客户端使用上一步获得的授权,向认证服务器申请令牌
(D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌
(E)客户端使用令牌,向资源服务器申请获取资源
(F)资源服务器确认令牌无误,同意向客户端开放资源
B是关键,即用户怎样才能给于客户端授权。有了这个授权以后,客户端就可以获取令牌,进而凭令牌获取资源
客户端获取授权的四种模式
客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。OAuth 2.0定义了四种授权方式
密码模式和授权码模式比较常用
授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与”服务提供商”的认证服务器进行互动
(A)用户访问客户端,后者将前者导向认证服务器
(B)用户选择是否给予客户端授权
(C)假设用户给予授权,认证服务器将用户导向客户端事先指定的”重定向URI”(redirection URI),同时附上一个授权码
(D)客户端收到授权码,附上早先的”重定向URI”,向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见
(E)认证服务器核对授权码和重定向URI,确认无误后,向客户端发送访问令牌access token和更新令牌refresh token
下面是上面这些步骤所需要的参数。
A步骤中,客户端申请认证的URI,包含以下参数:
下面是一个例子
> GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz > &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1 > Host: server.example.com >
C步骤中,服务器回应客户端的URI,包含以下参数:
下面是一个例子。
> HTTP/1.1 302 Found > Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA > &state=xyz >
D步骤中,客户端向认证服务器申请令牌的HTTP请求,包含以下参数:
下面是一个例子。
> POST /token HTTP/1.1 > Host: server.example.com > Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW > Content-Type: application/x-www-form-urlencoded > > grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA > &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb >
E步骤中,认证服务器发送的HTTP回复,包含以下参数:
下面是一个例子。
> HTTP/1.1 200 OK > Content-Type: application/json;charset=UTF-8 > Cache-Control: no-store > Pragma: no-cache > > { > "access_token":"2YotnFZFEjr1zCsicMWpAA", > "token_type":"example", > "expires_in":3600, > "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA", > "example_parameter":"example_value" > } >
从上面代码可以看到,相关参数使用JSON格式发送(Content-Type: application/json)。此外,HTTP头信息中明确指定不得缓存。
密码模式(Resource Owner Password Credentials Grant)中,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向”服务商提供商”索要授权
在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。而认证服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式
它的步骤如下:
(A)用户向客户端提供用户名和密码。
(B)客户端将用户名和密码发给认证服务器,向后者请求令牌。
(C)认证服务器确认无误后,向客户端提供访问令牌。
B步骤中,客户端发出的HTTP请求,包含以下参数:
下面是一个例子。
> POST /token HTTP/1.1 > Host: server.example.com > Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW > Content-Type: application/x-www-form-urlencoded > > grant_type=password&username=johndoe&password=A3ddj3w >
C步骤中,认证服务器向客户端发送访问令牌,下面是一个例子。
> HTTP/1.1 200 OK > Content-Type: application/json;charset=UTF-8 > Cache-Control: no-store > Pragma: no-cache > > { > "access_token":"2YotnFZFEjr1zCsicMWpAA", > "token_type":"example", > "expires_in":3600, > "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA", > "example_parameter":"example_value" > } >
简化模式(implicit grant type)不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,==跳过了”授权码”==这个步骤,因此得名。所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证
它的步骤如下:
(A)客户端将用户导向认证服务器。
(B)用户决定是否给于客户端授权。
(C)假设用户给予授权,认证服务器将用户导向客户端指定的”重定向URI”,并在URI的Hash部分包含了访问令牌。
(D)浏览器向资源服务器发出请求,其中不包括上一步收到的Hash值。
(E)资源服务器返回一个网页,其中包含的代码可以获取Hash值中的令牌。
(F)浏览器执行上一步获得的脚本,提取出令牌。
(G)浏览器将令牌发给客户端。
下面是上面这些步骤所需要的参数。
A步骤中,客户端发出的HTTP请求,包含以下参数:
下面是一个例子。
> GET /authorize?response_type=token&client_id=s6BhdRkqt3&state=xyz > &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1 > Host: server.example.com >
C步骤中,认证服务器回应客户端的URI,包含以下参数:
下面是一个例子。
> HTTP/1.1 302 Found > Location: http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA > &state=xyz&token_type=example&expires_in=3600 >
在上面的例子中,认证服务器用HTTP头信息的Location栏,指定浏览器重定向的网址。注意,在这个网址的Hash部分包含了令牌。
根据上面的D步骤,下一步浏览器会访问Location指定的网址,但是Hash部分不会发送。接下来的E步骤,服务提供商的资源服务器发送过来的代码,会提取出Hash中的令牌。
客户端模式(Client Credentials Grant)指客户端以自己的名义,而不是以用户的名义,向”服务提供商”进行认证。严格地说,客户端模式并不属于OAuth框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求”服务提供商”提供服务,其实不存在授权问题
我们对接微信公众号时,就采用的客户端模式。我们的后端服务器就扮演“客户端”的角色,与微信公众号的后端服务器进行交互。
比如 docker拉取镜像
删除 SecurityConfig 配置类,因为客户端模式下,无需 Spring Security 提供用户的认证功能。但是,Spring Security OAuth 需要一个 PasswordEncoder Bean,否则会报错,因此我们在 OAuth2AuthorizationServerConfig 类的 #passwordEncoder()
方法进行创建。
它的步骤如下:
(A)客户端向认证服务器进行身份认证,并要求一个访问令牌。
(B)认证服务器确认无误后,向客户端提供访问令牌。
A步骤中,客户端发出的HTTP请求,包含以下参数:
> POST /token HTTP/1.1 > Host: server.example.com > Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW > Content-Type: application/x-www-form-urlencoded > > grant_type=client_credentials >
认证服务器必须以某种方式,验证客户端身份。
B步骤中,认证服务器向客户端发送访问令牌,下面是一个例子。
> HTTP/1.1 200 OK > Content-Type: application/json;charset=UTF-8 > Cache-Control: no-store > Pragma: no-cache > > { > "access_token":"2YotnFZFEjr1zCsicMWpAA", > "token_type":"example", > "expires_in":3600, > "example_parameter":"example_value" > } >
上面代码中,各个参数的含义参见《授权码模式》一节。
如果用户访问的时候,客户端的”访问令牌”已经过期,则需要使用”更新令牌”申请一个新的访问令牌。
客户端发出更新令牌的HTTP请求,包含以下参数:
下面是一个例子。
> POST /token HTTP/1.1 > Host: server.example.com > Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW > Content-Type: application/x-www-form-urlencoded > > grant_type=refresh_token&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA >
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-09WZWOpV-1649940170695)(SpringSecurity.assets/image-20220329234113607.png)]
spring-cloud-demo的 oauth2-server
spring-cloud-demo中的oauth2-server
http://localhost:9401/oauth/authorize?response_type=code&client_id=admin&redirect_uri=http://www.baidu.com&scope=all
重定向uri 得到 授权码
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XhiFtyQe-1649940170695)(SpringSecurity.assets/image-20220331194332886.png)]
/oauth/token
对应 TokenEndpoint 端点
/oauth/check_token
端点对应 CheckTokenEndpoint 类,用于校验访问令牌的有效性。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dcc5ptVg-1649940170695)(SpringSecurity.assets/image-20220331194704871.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yXwQ3nPY-1649940170695)(SpringSecurity.assets/image-20220331194720726.png)]
{
"access_token": "0cd5dca9-f7ae-499e-b3da-5f75e296bf02",
"token_type": "bearer",
"expires_in": 43199,
"scope": "all"
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7poVrw2C-1649940170696)(SpringSecurity.assets/image-20220331194933177.png)]
{
"username": "admin",
"password": "$2a$10$bOe5PF1hKOaUjPjq3Fy/CuIFaVEwNWnzRHZ13RYi9OFkAD/fUnJkq",
"authorities": [
{
"authority": "admin"
},
{
"authority": "normal"
},
{
"authority": "ROLE_abc"
}
],
"enabled": true,
"credentialsNonExpired": true,
"accountNonExpired": true,
"accountNonLocked": true
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D2JGAN6z-1649940170696)(SpringSecurity.assets/image-20220402134248683.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qkUqDvtU-1649940170696)(SpringSecurity.assets/image-20220402134614308.png)]
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
<version>2.6.0version>
dependency>
密码模式下存储到redis
区别
JWTTokenStore | RedisTokenStore |
---|---|
token信息,认证信息封装在JWT负载中 | token信息,认证信息保存在Redis服务 |
token为加密的JWT负载 | token为随机redis key |
通过解密token获取用户认证信息 | 通过认证服务器校验token获取用户认证信息 |
优缺点
JWTTokenStore | RedisTokenStore |
---|---|
无需保存用户认证信息 | 使用Redis服务保存用户认证信息 |
无法撤销用户授权 | 可以撤销用户授权 |
create table oauth_client_details (
client_id VARCHAR(255) PRIMARY KEY,
resource_ids VARCHAR(255),
client_secret VARCHAR(255),
scope VARCHAR(255),
authorized_grant_types VARCHAR(255),
web_server_redirect_uri VARCHAR(255),
authorities VARCHAR(255),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additional_information VARCHAR(4096),
autoapprove VARCHAR(255)
);
create table if not exists oauth_client_token (
token_id VARCHAR(255),
token LONG VARBINARY,
authentication_id VARCHAR(255) PRIMARY KEY,
user_name VARCHAR(255),
client_id VARCHAR(255)
);
create table if not exists oauth_access_token (
token_id VARCHAR(255),
token LONG VARBINARY,
authentication_id VARCHAR(255) PRIMARY KEY,
user_name VARCHAR(255),
client_id VARCHAR(255),
authentication LONG VARBINARY,
refresh_token VARCHAR(255)
);
create table if not exists oauth_refresh_token (
token_id VARCHAR(255),
token LONG VARBINARY,
authentication LONG VARBINARY
);
create table if not exists oauth_code (
code VARCHAR(255), authentication LONG VARBINARY
);
create table if not exists oauth_approvals (
userId VARCHAR(255),
clientId VARCHAR(255),
scope VARCHAR(255),
status VARCHAR(10),
expiresAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
lastModifiedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO oauth_client_details
(client_id, client_secret, scope, authorized_grant_types,
web_server_redirect_uri, authorities, access_token_validity,
refresh_token_validity, additional_information, autoapprove)
VALUES
('clientapp', '112233', 'read_userinfo,read_contacts',
'password,refresh_token', null, null, 3600, 864000, null, true);
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-etchXeii-1649940170697)(SpringSecurity.assets/image-20220403190844103.png)]
在用户登出系统时,我们会有删除令牌的需求。虽然说,可以通过客户端本地删除令牌的方式实现。但是,考虑到真正的彻底的实现删除令牌,必然服务端自身需要删除令牌。
客户端本地删除令牌的方式实现,指的是清楚本地 Cookie、localStorage 的令牌缓存
在 Spring Security OAuth2 中,并没有提供内置的接口,所以需要自己去实现。笔者参看 《Spring Security OAuth2 – Simple Token Revocation》 文档,实现删除令牌的 API 接口。
具体的实现,通过调用 ConsumerTokenServices 的 #revokeToken(String tokenValue)
方法,删除访问令牌和刷新令牌。
public interface ConsumerTokenServices {
boolean revokeToken(String var1);
}
// 实现类
DefaultTokenServices
@RestController
@RequestMapping("/token/demo")
public class TokenDemoController {
@Autowired
private ConsumerTokenServices tokenServices;
/**
* 删除令牌
*
* @param token
* @return
*/
@PostMapping(value = "/revoke")
public boolean revokeToken(@RequestParam("token") String token) {
return this.tokenServices.revokeToken(token);
}
}
第一种,Session 黏连
使用 Nginx 实现会话黏连,将相同 sessionid 的浏览器所发起的请求,转发到同一台服务器。这样,就不会存在多个 Web 服务器创建多个 Session 的情况,也就不会发生 Session 不一致的问题。
不过,这种方式目前基本不被采用。因为,如果一台服务器重启,那么会导致转发到这个服务器上的 Session 全部丢失。
具体怎么实现这种方式,可以看看 《Nginx 第三方模块 —— nginx-sticky-module 的使用(基于cookie的会话保持)》 文章
第二种,Session 复制
Web 服务器之间,进行 Session 复制同步。仅仅适用于实现 Session 复制的 Web 容器,例如说 Tomcat 、Weblogic 等等。
不过,这种方式目前基本也不被采用。试想一下,如果我们有 5 台 Web 服务器,所有的 Session 都要同步到每一个节点上,一个是效率低,一个是浪费内存。
具体怎么实现这种方式,可以看看 [《Session 共享 —— Tomcat 集群 Session 复制》](https://www.iocoder.cn/Spring-Boot/Distributed-Session/session 共享-tomcat集群session复制) 文章
第三种,Session 外部化存储
不同于上述的两种方案,Session 外部化存储,考虑不再采用 Web 容器的内存中存储 Session ,而是将 Session 存储外部化,持久化到 MySQL、Redis、MongoDB 等等数据库中。这样,Tomcat 就可以无状态化,专注提供 Web 服务或者 API 接口,未来拓展扩容也变得更加容易。
而实现 Session 外部化存储也有两种方式:
① 基于 Tomcat、Jetty 等 Web 容器自带的拓展,使用读取外部存储器的 Session 管理器。例如说:
② 基于应用层封装 HttpServletRequest 请求对象,包装成自己的 RequestWrapper 对象,从而让实现调用 HttpServletRequest#getSession()
方法时,获得读写外部存储器的 SessionWrapper 对象。例如说,稍后我们会看到的本文的主角 Spring Session 。
Spring Session 提供了 SessionRepositoryFilter 过滤器,它会过滤请求时,将请求 HttpServletRequest 对象包装成 SessionRepositoryRequestWrapper 对象。代码如下:
// SessionRepositoryFilter.java
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// sessionRepository 是访问外部数据源的操作类,例如说访问 Redis、MySQL 等等
request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
// 将请求和响应进行包装成 SessionRepositoryRequestWrapper 和 SessionRepositoryResponseWrapper 对象
SessionRepositoryFilter.SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryFilter.SessionRepositoryRequestWrapper(request, response, this.servletContext);
SessionRepositoryFilter.SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryFilter.SessionRepositoryResponseWrapper(wrappedRequest, response);
// 继续执行下一个过滤器
try {
filterChain.doFilter(wrappedRequest, wrappedResponse);
} finally {
// 请求结束,提交 Session 到外部数据源
wrappedRequest.commitSession();
}
}
调用 SessionRepositoryRequestWrapper#getSession()
方法时,返回的是自己封装的 HttpSessionWrapper 对象。代码如下:
// SessionRepositoryFilter#SessionRepositoryRequestWrapper.java
@Override
public HttpSessionWrapper getSession() {
return getSession(true);
}
后续,我们调用 HttpSessionWrapper 的方法,例如说 HttpSessionWrapper#setAttribute(String name, Object value)
方法,访问的就是外部数据源,而不是内存中。
当然 ① 和 ② 两种方案思路是类似且一致的,只是说拓展的提供者和位置不同。相比来说,② 会比 ① 更加通用一些
springcloud-demo中的oauth2-jwt-server
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b9pQW0cp-1649940170697)(SpringSecurity.assets/image-20220402192121097.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-naiVKVg8-1649940170697)(SpringSecurity.assets/image-20220402192130157.png)]
解析
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kTrOz7FW-1649940170697)(SpringSecurity.assets/image-20220402193138735.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g7ZhPOgO-1649940170697)(SpringSecurity.assets/image-20220402193123166.png)]
刷新令牌
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NmvdeeuG-1649940170698)(SpringSecurity.assets/image-20220402193648227.png)]
springcloud-demo中的oauth2-client
https://snailclimb.gitee.io/javaguide/#/docs/system-design/authority-certification/SSO%E5%8D%95%E7%82%B9%E7%99%BB%E5%BD%95%E7%9C%8B%E8%BF%99%E4%B8%80%E7%AF%87%E5%B0%B1%E5%A4%9F%E4%BA%86
SSO(Single Sign On)即单点登录说的是用户登陆多个子系统的其中一个就有权访问与其相关的其他系统。举个例子我们在登陆了京东金融之后,我们同时也成功登陆京东的京东超市、京东国际、京东生鲜等子系统
@EnableOAuth2Sso
http://localhost:9501/user/getCurrentUser
{
"authorities": [
{
"authority": "ROLE_abc"
},
{
"authority": "admin"
},
{
"authority": "normal"
}
],
"details": {
"remoteAddress": "0:0:0:0:0:0:0:1",
"sessionId": "70A88E519DD83A9F820AE27D1FB5D1B1",
"tokenValue": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE2NDg5ODgxNDcsImF1dGhvcml0aWVzIjpbIlJPTEVfYWJjIiwiYWRtaW4iLCJub3JtYWwiXSwianRpIjoiODFkZmQwOWYtZTk1YS00ODEyLWIzMjUtNGFjNmRkZjQ5ZmVjIiwiY2xpZW50X2lkIjoiYWRtaW4iLCJlbmhhbmNlIjoiZW5oYW5jZSBpbmZvIn0.Id9D9AsA2pvq0GlfWaznMuQY29Sni_os9NGGwF81gUo",
"tokenType": "bearer",
"decodedDetails": null
},
"authenticated": true,
"userAuthentication": {
"authorities": [
{
"authority": "ROLE_abc"
},
{
"authority": "admin"
},
{
"authority": "normal"
}
],
"details": null,
"authenticated": true,
"principal": "admin",
"credentials": "N/A",
"name": "admin"
},
"clientOnly": false,
"credentials": "",
"principal": "admin",
"oauth2Request": {
"clientId": "admin",
"scope": [
"all"
],
"requestParameters": {
"client_id": "admin"
},
"resourceIds": [
],
"authorities": [
],
"approved": true,
"refresh": false,
"redirectUri": null,
"responseTypes": [
],
"extensions": {
},
"grantType": null,
"refreshTokenRequest": null
},
"name": "admin"
}
postman接口单点登录
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EFj7cG2m-1649940170698)(SpringSecurity.assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly90aGlua3dvbi5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XDUSIaex-1649940170698)(SpringSecurity.assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly90aGlua3dvbi5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2dj0dIPQ-1649940170698)(SpringSecurity.assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly90aGlua3dvbi5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WYv74Mn4-1649940170699)(SpringSecurity.assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly90aGlua3dvbi5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70.png)]
oauth2-client添加权限校验
参考:https://www.iocoder.cn/Fight/user-authentication-with-jwt/?self
https://www.iocoder.cn/Spring-Security/OAuth2-learning-sso/?github
使用JWT的方式则没有这个问题的存在,因为用户的状态已经被传送到了客户端。因此,我们只需要将含有JWT的Cookie的domain
设置为顶级域名即可,例如
Set-Cookie: jwt=lll.zzz.xxx; HttpOnly; max-age=980000; domain=.taobao.com
注意domain
必须设置为一个点加顶级域名,即.taobao.com
。这样,taobao.com和*.taobao.com就都可以接受到这个Cookie,并获取JWT了。
– oauth_access_token OAuth 2.0 访问令牌
– oauth_refresh_token OAuth 2.0 刷新令牌
– oauth_code OAuth 2.0 授权码
– oauth_client_details OAuth 2.0 客户端
– oauth_client_token
– oauth_approvals
– users 用户表
– authorities 授权表,例如用户拥有的角色
参考 spring-boot-demo-spring-security-jwt-rbac