目录
课程名称: Spring Security+OAuth2 精讲,打造企业级认证与授权
课程章节:第二章 初识SpringSecurity
课程讲师:接灰的电子产品
课程内容:
学习目标
-
能够说出认证和授权的概念
-
能够简单表述或用代码的演示资源的认证和授权流程
-
能够简单表述过滤器链的作用
-
了解SpringSecurity过滤器链的大致流程和常见的过滤器链
-
熟悉HTTP请求与响应的结构
-
熟悉HTTP Basic Auth认证流程
-
可以解释什么是CSRF攻击
-
可以处理登入登出的返回结果
-
可以自定义过滤器链,实现某些功能
-
可以看懂SecurityConfig中关于configure(HttpSecurity http) 的配置
-
可以使用和配置Spring国际化
认证授权的概念
1.1 本章概述
| 认证和授权的概念 | Filter和FilterChain | Http | 实战 |
| :------------------------: | :--------------------------------------: | :-----------------------------------------------------------------------: | :--------------------------------------------------------: |
| ·认证解决“我是谁”的问题 | -Spring Security实现认证和授权的底层机制 | ·熟悉Http 的请求/响应的结构 | .新建工程·依赖类库 |
| 授权解决“我能做什么"的问题 | | ·Filter和窑户端交互(获取数据看,返回数据)是通过请求/响应中的字段完成的。 | 安全配置定制化登录页·CSRF攻击和保护·登录成功和失败后的处理 |
1.2 什么是认证
认证(Authentication)
1.3 什么是授权
授权(Authorization)
1.4 认证和授权过程的简单代码实现
1.定义资源
@RestController
@RequestMapping("/authorize")
public class AuthorizeResource {
@GetMapping(value="greeting")
public String sayHello() {
return "hello world";
}
}
2.导入依赖
<!-- 用于开发 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
3.访问该资源
4.授权的过程
@Slf4j
@RequiredArgsConstructor
@EnableWebSecurity(debug = true)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final ObjectMapper objectMapper;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests -> authorizeRequests
.antMatchers("/authorize/**").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/api/**").hasRole("USER")
.anyRequest().authenticated())
.addFilterAt(restAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
2.2 过滤器和过滤器链
2.2.1 Spring Filters
任何Spring Web应用本质上只是一个 servlet
Security Filter在 HTTP请求到达你的Controller 之前过滤每一个传入的HTTP请求
2.2.2 过滤器示例
-
首先,过滤器需要从请求中提取一个用户名/密码。它可以通过一个基本的HTTP头,或者表单字段,或者
cookie
等等。 -
然后,过滤器需要对用户名/密码组合进行验证比如数据库。
-
在验证成功后,过滤器需要检查用户是否被授权访问请求的URI。
-
如果请求通过了所有这些检查,那么过滤器就可以让请求通过你的
DispatcherServlet
后重定向到@Controllers
或者@RestController
。
2.2.3 Filter Chain
登录访问过滤器,认证过滤器,授权过滤器
2.2.4 常见的内建过滤器
BasicAuthenticationFilter:
·如果在请求中找到一个Basic Auth HTTP头,如果找到,则尝试用该头中的用户名和密码验证用户。
UsernamePasswordAuthenticationFilter
·如果在请求参数或者POST的Request Body中找到用户名/密码,则尝试用这些值对用户进行身份验证。
DefaultLoginPageGeneratingPtlter
·默认登录页面生成过滤器。用于生成一个登录页面,如果你没有明确地禁用这个功能,那么就会生成一个登录页面。这就是为什么在启用Spring Security时,会得到一个默认登录页面的原因。
DefaultLogoutPageGeneratingFilter
·如果没有禁用该功能,则会生成一个注销页面。
FilterSecurityInterceptor
·过滤安全拦截器。用于授权逻辑。
2.2.5 其他过滤器
JWTFilter(第五章会讲到的自建FIlter过滤器)
用于JWTToken的验证处理
3 http 请求的结构
3.1 HTTP请求
3.2 HTTPBasicAuth认证方式
通过Vscode restClient模仿不同的rest请求
@password=0ebaa294-cc23-45df-977c-ebbe40e12123
### Get请求
GET http://localhost:8080/api/greeting HTTP/1.1
### 带BasicAuth 的get认证请求
GET http://localhost:8080/api/greeting HTTP/1.1
Authorization: Basic user {{password}}
### 带QueryParam 查询参数的POST请求
POST http://localhost:8080/api/greeting?name=王五
Authorization: Basic user {{password}}
Content-Type: application/json
{
"gender": "男",
"idNo": "22323232323"
}
### 路径参数
PUT http://localhost:8080/api/greeting/王五
Authorization: Basic user {{password}}
###
POST http://localhost:8080/authorize/login
Content-Type: application/json
{
"username": "user",
"password": "1234567"
}
4 HTTP响应和HTTP Basic Auth
4.1 HTTP响应
一个典型的状态行:HTTP/1.1 201 Created。
-
General Headers 与每次响应无关,是全局的设置
-
Response Header 与每一个响应相关
-
Entity Headers
4.2 HTTP Basic Auth认证流程
认证流程:
-
客户端发送对某个资源(URL)的请求
-
服务器会返回一个401表示未授权,并在响应里添加
WWW-Authenticate:Basic realm="Access to the staging site"
。 -
浏览器收到响应后会重定向到登录表单页面。或者浏览器弹出一个窗口让你填写用户名和密码。
-
当填写了密码后,浏览器会将密码进行Basic64编码
-
客户端接收信息后对用户名密码进行验证。
-
验证成功返回自定资源
HTTP base Auth 是表单验证的一种方式.
5 安全配置
5.1 HttpSecurity安全配置
配置 Spring Security
WebSecurityConfigurerAdapter 中的configure(HttpSecurity)
安全配置主要分为:
-
认证请求配置 authorizeRequests()
-
表单登录部分 formLogin()
-
表单认证 httpBasic()
protected void configure(HttpSecurity http) throws Exception {
logger.debug ("Using default configure(HttpSecurity). If subclas");
http
.authorizeRequests()
.anyRequest().authenticated() //任何请求都会进行认证
.and()
.formLogin() //启用内建的登录界面.and ()
.httpBasic(); //使用HTTP Basic Auth 进行认证
}
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests -> authorizeRequests
.antMatchers("/authorize/**").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/api/**").hasRole("USER")
.anyRequest().authenticated())
.addFilterAt(restAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.formLogin(login -> login
.loginPage("/login")
//.usernameParameter("username1") //表单登录参数 默认为username
//.loginProcessingUrl("/login1") //表单action参数 默认为login
//.rememberMe(rememberMe -> rememberMe.rememberMeParameter)
.failureHandler(jsonLoginFailureHandler())
.successHandler(new UaaSuccessHandler())
// .successHandler(jsonLoginSuccessHandler())
// .defaultSuccessUrl("/")
.permitAll())
.logout(logout -> logout
.logoutUrl("/perform_logout")
// .logoutSuccessUrl("/login")
.logoutSuccessHandler(jsonLogoutSuccessHandler())
)
.rememberMe(rememberMe -> rememberMe
.key("someSecret")
.tokenValiditySeconds(86400))
.httpBasic(Customizer.withDefaults()); // 显示浏览器对话框,需要禁用 CSRF ,或添加路径到忽略列表
}
rememberMe()见
.httpBasic(Customizer.withDefaults())启用了HTTPBasic,我们可以有一个认证头
5.2 application.yml 配置默认用户
spring:
messages:
basename: messages
encoding: UTF-8
security:
user:
name: user
password: 12345678
roles: USER,ADMIN
5.3 WebSecurity安全配置
配置静态文件地址、在HTTPSecurity过滤器前面。将一些指定地址的资源,和常见的公共资源放开。
@Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
.antMatchers("/public/**")
.requestMatchers(PathRequest.toStaticResources().atCommonLocations());
}
6 自定义登录页
6.1 SpringResourceBundle国际化
因为有很多文本信息,所以需要建立一个resourceBundle处理国际化。
Spring新建国际化包:
resource -> New -> resource bundle
国际化配置文件:
配置 application.yml
basename和新建时填写的basename保持一致
spring:
messages:
always-use-message-format: false
basename: message
encoding: UTF-8
fallback-to-system-locale: true
use-code-as-default-message: false
6.2 国际化的参数和使用
6.2.1 前台参数
###
# @name register
POST {{host}}/authorize/register
Accept-Language: zh-CN
Content-Type: application/json
{
"name": "张三李四",
"username": "zhangsan",
"password": "qwerty12345T!",
"matchingPassword": "12345678",
"email": "zs@local"
}
6.2.2 后台接收
@PostMapping("/register")
public void register(@Valid @RequestBody UserDto userDto, Locale locale) {
if (userService.isUsernameExisted(userDto.getUsername())) {
throw new DuplicateProblem("Exception.duplicate.username", messageSource, locale);
}
if (userService.isEmailExisted(userDto.getEmail())) {
throw new DuplicateProblem("Exception.duplicate.email", messageSource, locale);
}
if (userService.isMobileExisted(userDto.getMobile())) {
throw new DuplicateProblem("Exception.duplicate.mobile", messageSource, locale);
}
val user = User.builder()
.username(userDto.getUsername())
.name(userDto.getName())
.email(userDto.getEmail())
.mobile(userDto.getMobile())
.password(userDto.getPassword())
.build();
userService.register(user);
}
7 csrf,logout和rememberMe的设置
7.1 CSRF攻击是什么?
你已经登录了一个站点,站点的登录状态有session,并且你的session还在有效期之内,恶意用户会发一个恶意链接,盗取你的账户名和密码生成一个表单信息,向真正的站点发送提交。
7.2 防止受到CSRF攻击的方式
7.2.1 CSRF Token方式
-
由服务端生成并设置一个CSRF token到浏览器前端Cookie中
-
前端读取Cookie的CSRF token添加到表单中。
-
后台读取请求后验证CSRF token是否一致。
7.2.2 在响应中设置Cookie的SameSite属性
无状态访问的请求对CSRF攻击天然免疫。
7.3 Remember-me功能
为解决session过期后用户的直接访问问题
Spring Security 提供开箱即用的配置rememberMe
原理∶使用Cookie存储用户名,过期时间,以及一个 Hash
Hash : md5(用户名+过期时间+密码+key)
8. 登录成功及失败的处理
8.1.定制登录/退出登录的处理
定制登录/退出登录的处理。即将返回值结果以指定的JSON格式返回。方便无状态应用的使用.
登录成功后的处理:AuthenticationSuccessHandler
private AuthenticationSuccessHandler jsonLoginSuccessHandler() {
return (req, res, auth) -> {
ObjectMapper objectMapper = new ObjectMapper();
res.setStatus(HttpStatus.OK.value());
res.getWriter().println(objectMapper.writeValueAsString(auth));
log.debug("认证成功");
};
}
登录失败后的处理:AuthenticationFailureHandler
private AuthenticationFailureHandler jsonLoginFailureHandler() {
return (req, res, exp) -> {
res.setStatus(HttpStatus.UNAUTHORIZED.value());
res.setContentType(MediaType.APPLICATION_JSON_VALUE);
res.setCharacterEncoding("UTF-8");
val errData = Map.of(
"title", "认证失败",
"details", exp.getMessage()
);
res.getWriter().println(objectMapper.writeValueAsString(errData));
};
}
退出登录成功后的处理:LogoutSuccessHandler
private LogoutSuccessHandler jsonLogoutSuccessHandler() {
return (req, res, auth) -> {
if (auth != null && auth.getDetails() != null) {
req.getSession().invalidate();
}
res.setStatus(HttpStatus.OK.value());
res.getWriter().println();
log.debug("成功退出登录");
};
}
9 自定义Filter
要求会自定义一个Filter,实现对用户请求的拦截和数据的解析,并将其加入到SpringSecurity的过滤器链里。
Spring推荐使用构造函数进行注入。 自定义一个用户名密码授权过滤器。
9.1 自定义filter
@RequiredArgsConstructor
public class RestAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final ObjectMapper objectMapper;
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
UsernamePasswordAuthenticationToken authRequest;
try (InputStream is = request.getInputStream()) {
val jsonNode = objectMapper.readTree(is);
String username = jsonNode.get("username").textValue();
String password = jsonNode.get("password").textValue();
authRequest = new UsernamePasswordAuthenticationToken(username, password);
} catch (IOException e) {
throw new BadCredentialsException("没有找到用户名或密码参数");
}
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
9.2 实例化过滤器链
-
实例化过滤器链
-
设置成功和失败的处理器
-
设置authenticationManager()
-
设置于某个应用的URL
private RestAuthenticationFilter restAuthenticationFilter() throws Exception {
RestAuthenticationFilter filter = new RestAuthenticationFilter(objectMapper);
filter.setAuthenticationSuccessHandler(jsonLoginSuccessHandler());
filter.setAuthenticationFailureHandler(jsonLoginFailureHandler());
filter.setAuthenticationManager(authenticationManager());
filter.setFilterProcessesUrl("/authorize/login");
return filter;
}
9.3 将过滤器链替代指定的过滤器链
可以在指定的过滤器链之前或之后添加过滤器链,也可以替换该过滤器链。
.addFilterAt(restAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)