如何伪装成一个服务端开发SpringSecurity
简介
一个简单的总结就是,利用Filter(servlet)进行访问权限控制的spring 安全组件。
Spring Security 在进入到 DispatcherServlet 前就可以对 Spring MVC 的请求进行拦截,然后通过一定的验证,从而决定是否放行请求访问系统。
引入依赖
对于Spring boot项目而言,只需要简单引入依赖即可
<dependency> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
然后直接启动应用(创建了一个@Controller,地址/test/test)。
在启动过程中,会看到控制台打印出了一串密码
然后浏览器打开 127.0.0.1:8080/test/test , 发现被转链到了 http://127.0.0.1:8080/login
用户名user , 密码就是刚才的密码,登录后就会跳转到原链接。
可以在 application.properties中配置用户名和密码
spring.security.user.name=user
spring.security.user.password=123456
史上最简单的应用,带你领略了一下Spring Security 的初步使用。
Sping Security 还可以进行如下设置
# SECURITY (SecurityProperties)# Spring Security过滤器排序
spring.security.filter.order=-100
# 安全过滤器责任链拦截的分发类型
spring.security.filter.dispatcher-types=async,error,request
# 用户名,默认值为user
spring.security.user.name=user
# 用户密码
spring.security.user.password=
# 用户角色
spring.security.user.roles=
# SECURITY OAUTH2 CLIENT (OAuth2ClientProperties)
# OAuth提供者详细配置信息
spring.security.oauth2.client.provider.*= #
# OAuth客户端登记信息
spring.security.oauth2.client.registration.*=
自定义 WebSecurityConfigurerAdapter
先不聊底层原理,后头其他系列再补
WebSecurityConfigurerAdapter 中的 configure方法是一个用于配置用户信息的方法,默认没有任何配置。在spring boot中如果没有配置用户信息,就会自动生成一个用户名为user,密码随机的用户。
内存签名服务
在项目中新增如下文件
@Configurationpublic class CoupleWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 密码编码器
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
// 使用内存存储
auth.inMemoryAuthentication()
// 设置密码编码器
.passwordEncoder(passwordEncoder)
// 注册用户 admin,密码为 abc,并赋予 USER 和 ADMIN 的角色权限
.withUser("admin")
// 可通过 passwordEncoder.encode("abc")得到加密后的密码
.password("$2a$10$5OpFvQlTIbM9Bx2pfbKVzurdQXL9zndm1SrAjEkPyIuCcZ7CqR6je")
.roles("USER", "ADMIN")
// 连接方法 and
.and()
// 注册用户 myuser,密码为123456,并赋予 USER 的角色权限
.withUser("myuser")
// 可通过 passwordEncoder.encode("123456")得到加密后的密码
.password("$2a$10$ezW1uns4ZV63FgCLiFHJqOI6oR6jaaPYn33jNrxnkHZ.ayAFmfzLS")
.roles("USER");
}
}
AuthenticationManagerBuilder 参与用于保存用户信息,比如想要保存在内存中,就调用inMemoryAuthentication方法,会返回一个内存用户信息管理类,然后往其中填入用户名,密码,角色信息。
为了防止设置123456这种简单密码,服务端一般都会加上一个密钥存储,与原密码合并存储
在 application.properties中定义一个配置
system.user.password.secret=yb;bkkfsff_3
然后修改使用的加密类
// 密码编码器 PasswordEncoder passwordEncoder = new Pbkdf2PasswordEncoder(this.secret);
PS: roles方法会自动在字符串前机上 ROLE_(如果没有手动加的话)
PS: 用户创建用户的类为 UserDetailsBuilder,有如下方法可用
项 目 类 型 描 述
accountExpired(boolean)
设置账号是否过期
accountLocked(boolean)
是否锁定账号
credentialsExpired(boolean)
定义凭证是否过期
disabled(boolean)
是否禁用用户
username(String)
定义用户名
authorities(GrantedAuthority... )
赋予一个或者权限
authorities(List<? extends GrantedAuthority>)
使用列表(List)赋予权限
password(String)
定义密码
roles(String...)
赋予角色,会自动加入前缀“ROLE_”
使用数据库定义的用户认证服务
1 物料准备,自己创建一个数据库,定义一个用户表吧
2 在项目中配置数据库连接 参考
3 重写 configure方法
@Resource(name = "dataSource") //不能用 @Autowired 因为 Autowired 是byType注入,会报错。 private DataSource dataSource;
@Value("${system.user.password.secret}")
private String secret = null;
// 使用用户名称查询密码
String pwdQuery = "SELECT username,pwd,1 FROM account where username = ?";
// 使用用户名称查询角色信息
String roleQuery = "SELECT account.username, role_type.type from account , role_type, t_user_role WHERE t_user_role.account_id = account.id and
" +
"t_user_role.role_id = role_type.id and account.username = ?";
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 密码编码器
PasswordEncoder passwordEncoder = new Pbkdf2PasswordEncoder(this.secret);
auth.jdbcAuthentication()
.passwordEncoder(passwordEncoder)
.dataSource(dataSource)
.usersByUsernameQuery(pwdQuery)
.authoritiesByUsernameQuery(roleQuery);
}
这里比较重要的事情是 pwdQuery需要返回三个字段:用户名,密码,是否可用
roleQuery需要返回两个字段: 用户名,userrole
自定义用户认证服务
很多时候,简单的使用内存或者数据库并不满足我们的需求。比如NoSQL的引入。假设现在我们逻辑是先从redis上获取,如果没有再从 sql中获取。
首先需要继承 UserDetailServie 定义一个服务
@Servicepublic class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
UserDO userDO = userService.getUser(s);
if(userDO == null){
throw new UsernameNotFoundException(s);
}
return changeToUser(userDO);
}
private UserDetails changeToUser(UserDO userDO) {
// 权限列表
List<GrantedAuthority> authorityList = new ArrayList<>();
// 赋予查询到的角色,注意需要主动添加 ROLE_否则后面鉴权会出错
GrantedAuthority authority
= new SimpleGrantedAuthority("ROLE_"+userDO.getRole().getType());
authorityList.add(authority);
// 创建 UserDetails 对象,设置用户名、密码和权限
UserDetails userDetails = new User(userDO.getUserName(),
userDO.getPwd(), authorityList);
return userDetails;
}
}
PS: 假设 userService.getUser实现了先从redis获取用户信息,如果没获取到,那就从数据库获取。
然后修改 CoupleWebSecurityConfigurerAdapter
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 密码编码器
PasswordEncoder passwordEncoder = new Pbkdf2PasswordEncoder(this.secret);
// 设置用户密码服务和密码编码器
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder);
}
权限访问控制
上面的实例中描述了如何对用户身份进行验证。但是存在一个问题,一旦实现了上述功能,就会导致所有的链接都需要进行身份验证。在实际使用中,这并不是我们需要的,甚至很大一部分的地址实际上是并不需要登录的。
这时候就需要通过重写 WebSecurityConfigurerAdapter
中的 configure(HttpSecurity http)
方法。
该方法的默认实现如下
// WebSecurityConfigurerAdapter默认设定访问权限和登录方式protected void configure(HttpSecurity http) throws Exception {
.....
// 表示对于所有请求,都需要进行身份验证
http.authorizeRequests().anyRequest().authenticated()
// and方法是连接词,formLogin代表使用Spring Security默认的登录界面
.and().formLogin()
// httpBasic方法说明启用HTTP基础认证
.and().httpBasic();
}
我们可以通过ant风格对路径进行限定
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//对于 user 路径下的地址,需要有user 或者admin权限
//hasAnyRole会自动添加 ROLE_前缀
.antMatchers("/user/**").hasAnyRole("user", "admin")
//对于admin路径下的地址,需要admin的权限
.antMatchers("/admin/**").hasAuthority("ROLE_admin")
//除上面列出的路径,其他路径允许任何人访问,不需要登录
.anyRequest().permitAll()
.and().formLogin()
.and().httpBasic();
}
方法 含义
authorizeRequests
方法在这条链上相当于宣告开始进行路径权限配置。
antMatchers
方法是值使用 ant风格进行路径地址匹配。
hasAnyRole和hasAuthority
表示对匹配到的路径进行身份权限限定
anyRequest()
表示匹配任何请求
permitAll()
表示无条件允许访问
authenticated()
表示只有登录了的用户能够访问
anonymous()
允许匿名访问。spring会自动设置该条件,可以主动禁用(disable)。
denyAll()
无条件禁止任何访问
rememberme()
启用remembeme功能
fullyAuthenticated()
是完整验证(不是通过remember-me功能)
PS: authorizeRequests实际上是会要求所有访问者都是登录的。那么为什么通过anyRequest().permitAll() 能够使未登录用户也能访问所有连接呢?因为当开启了anonymous()之后所有用户都是登录用户,如果你没有主动登录,那么就是匿名登录状态,拥有 ROLE_ANONYMOUS权限。可以尝试加上anonymous().disable(),会发现原本不需要登录的链接也需要登录了。因为我们禁止了匿名登录,这时,未登录用户就真的是未登录了,用户数据为空。
PS:除了通过ant匹配,还可以通过正则表达式匹配,使用regexMatchers方法。
PS:如果你想debug,可以尝试在FilterSecurityInterceptor的doFilter方法上添加断点。
访问限定进阶
有时候我们可能会有更加复杂的权限限定机制。这时候单纯使用hasAuthority这种方法无法满足我们的需求。
所有spring 还提供了一个access方法,他允许我们使用Spring EL进行配置,比如。
@Overrideprotected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 使用 Spring 表达式限定只有角色ROLE_USER 或者 ROLE_ADMIN
.antMatchers("/user/**").access("hasRole("USER") or hasRole("ADMIN")")
// 设置访问权限给角色 ROLE_ADMIN,要求是完整登录(非记住我登录)
.antMatchers("/admin/welcome1").
access("hasAuthority("ROLE_ADMIN") && isFullyAuthenticated()")
// 限定"/admin/welcome2"访问权限给角色 ROLE_ADMIN,允许不完整登录
.antMatchers("/admin/welcome2").access("hasAuthority("ROLE_ADMIN")")
// 使用记住我的功能
.and().rememberMe()
// 使用 Spring Security 默认的登录页面
.and().formLogin()
// 启动 HTTP 基础验证
.and().httpBasic();
}
出自上面写出的表达式外,Spring EL还提供了如下几个表达式
|方法|含义|
|--|--|
|authentication() |用户认证对象|
|denyAll() |拒绝任何访问|
|hasAnyRole(String ...) |当前用户是否存在参数中列明的对象属性|
|hasRole(String) |当前用户是否存在角色|
|hasIpAddress(String) |是否请求来自指定的 IP|
|isAnonymous() |是否匿名访问|
|isAuthenticated() |是否用户通过认证签名|
|isFullyAuthenticated() |是否用户是完整验证,即非“记住我”(Remember Me认证)功能通过的认证|
|isRememberMe() |是否是通过“记住我”功能通过的验证|
|permitAll()|无条件允许任何访问|
|principal()|用户的 principal 对象|
强制使用https
在实际使用中,部分站点地址是需要谨慎保护的,这时候我们会选择使用https协议采用证书加密的方式进行访问。这时候就会需要对部分地址进行限定,只接受https请求
比如
//使用Https请求 http.requiresChannel().antMatchers("/admin/**").requiresSecure()
//不使用https请求
.and().requiresChannel().antMatchers("/user/**").requiresInsecure()
.and().authorizeRequests()
.antMatchers("/user/**").hasAnyRole("user", "admin")
.antMatchers("/admin/**").hasAuthority("ROLE_admin")
.anyRequest().permitAll()
.and().rememberMe()
.and().formLogin()
.and().httpBasic();
PS: 不过想使用https功能,还需要配置https证书啊啥的。这里就先不介绍了。
CSRF
关于CSRF的定义可以查看 CSRF 跨站点请求伪造
spring security默认开启了 csrf功能。最直接的表现可以从login接口上看到
login接口会带上csrf参数(和是在页面post这个表单的时候添加上去的.)
PS:一旦开启csrf,对于所有的 post 请求都是需要写入 csrf参数(或者head)的。对于get不需要。 逻辑实际上可配置。
具体原理和额外配置以后再聊。
自定义登录页面
上述的安全登录都是以 Spring Security 默认的登录页面,实际上,更多的时候需要的是自定义的登录页。有时候还需要一个“记住我”功能,避免用户在自己的客户端每次都需要输入密码。关于这些,Spring Security 都提供了进行管理的方法。代码清单12-16所示的代码将通过覆盖 WebSecurity ConfigurerAdapter 的 configure(HttpSecurity)方法,让登录页指向对应的请求路径和启用“记住我”(Remember Me)功能。
@Overrideprotected void configure(HttpSecurity http) throws Exception {
http
// 访问/admin 下的请求需要管理员权限
.authorizeRequests().antMatchers("/admin/**").access("hasRole("ADMIN")")
// 启用remember me功能
.and().rememberMe().tokenValiditySeconds(86400).key("remember-me-key")
// 启用 HTTP Batic 功能
.and().httpBasic()
// 通过签名后可以访问任何请求
.and().authorizeRequests().antMatchers("/**").permitAll()
// 设置登录页和默认的跳转路径
.and().formLogin().loginPage("/login/page")
.defaultSuccessUrl("/admin/welcome1");
}
这里的 rememberme 方法意思为启用了“记住我”功能,这个“记住我”的有效时间为1天(86 400 s),而在浏览器中将使用 Cookie 以键“remember-me-key”进行保存,只是在保存之前会以 MD5 加密,这样就能够在一定程度上对内容进行保护。loginPage 方法是指定登录路径为“login/page”,defaultSuccessUrl 方法是指定默认的跳转路径为“admin/welcome1”。
这样需要指定 longin/page 所映射的路径,我们可以使用传统的控制器去映射,也可以使用新增的映射关系去完成
package com.springboot.chapter12.config;/**** imports ****/
@Configuration
public class WebConfig implements WebMvcConfigurer {
// 增加映射关系
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// 使得/login/page映射为login.jsp
registry.addViewController("/login/page").setViewName("login");
// 使得/logout/page映射为logout_welcome.jsp
registry.addViewController("/logout/page").setViewName("logout_welcome");
// 使得/logout映射为logout.jsp
registry.addViewController("/logout").setViewName("logout");
}
}
代码中 WebConfig 类实现了 WebMvcConfigurer 接口,并覆盖了 addViewControllers 方法。在方法里存在3个路径的配置,但这里只讨论注册的 URL,即“/login/page”,它映射为“login.jsp”,后面的两个路径后续会再讨论。这里给出这个 JSP 的代码,如代码清单12-18所示。
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>自定义登录表单</title>
</head>
<body>
<form action="/login/page" method="POST">
<p>名称:<input id="username" name="username" type="text" value=""/></p>
<p>描述:<input id="password" name="password" type="password" value=""/></p>
<p>记住我:<input id="remember_me" name="remember-me" type="checkbox"></p>
<p><input type="submit" value="登录"></p>
<input type="hidden" id="${_csrf.parameterName}"
name="${_csrf.parameterName}" value="${_csrf.token}"/>
</form>
</body>
</html>
请注意这个表单的字段定义,这里的表单(form)提交的 action 定义为“/login/page”,这里安全登录拦截器就会拦截这些参数了,这里要求 method 为“POST”,不能是“GET”。表单中定义用户名且要求参数名称为 username,密码为 password,“记住我”为remember-me,且“记住我”是一个 checkbox。这样提交到登录 URL 的时候,Spring Security 就可以获取这些参数,只是要切记这里的参数名是不能修改的。之前我们讨论过,Spring Boot中CRSF 过滤器是会默认启动的,因此这里还会在请求表单中加入了对应的参数,这样就可以避免 CSRF 的攻击了。通过上面的代码,就可以实现自定义登录页面和启用“记住我”功能。
# 登出
有了登录,自然就会有登出。对于默认的情况下,Spring Security 会提供一个 URL——“/logout”,只要使用 HTTP的POST 请求(注意,GET 请求是不能退出的)了这个 URL,Spring Security 就会登出,并且清除 Remember Me 功能保存的相关信息。有时候也想自定义请求退出的路径。如代码清单12-17将请求“/logout/page”映射为“logoutwelcome.jsp”,作为登出后的欢迎页面,这样便需要开发 logoutwelcome.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%><%@ taglib prefix="c" uri="http://java.sun.com/jstl/core"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Spring Security 登出</title>
</head>
<body>
<h2>您已经登出了系统/h2>
</body>
</html>
它还将请求将“/logout”映射为“logout.jsp”,作为测试登出的页面,于是这里还需要开发这个页面
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>登出</title>
</head>
<body>
<form action="/logout/page" method="POST">
<p><input type="submit" value="登出"></p>
<input type="hidden" id="${_csrf.parameterName}"
name="${_csrf.parameterName}" value="${_csrf.token}"/>
</form>
</body>
</html>
这里的表单(form)定义将提交路径设置为“/logout/page”,方法为 POST(不能为GET),并且表单中还有 CSRF 的 token 参数。为了使 Spring Security 的 LogoutFilter 能够拦截这个动作的请求,需要修改 WebSecurityConfigurerAdapter 的方法 configure(HttpSecurity)
@Overrideprotected void configure(HttpSecurity http) throws Exception {
http
// 访问/admin 下的请求需要管理员权限
.authorizeRequests().antMatchers("/admin/**")
.access("hasRole("ADMIN")")
// 通过签名后可以访问任何请求
.and().authorizeRequests()
.antMatchers("/**").permitAll()
// 设置登录页和默认的跳转路径
.and().formLogin().loginPage("/login/page")
.defaultSuccessUrl("/admin/welcome1")
// 登出页面和默认跳转路径
.and().logout().logoutUrl("/logout/page")
.logoutSuccessUrl("/welcome");
}
在加粗代码中,定义了成功登出跳转的路径为“/welcome”,而登出的请求 URL 为“/logout/page”。这样当使用 POST 方法请求“/logout/page”的时候,Spring Security 的过滤器 LogoutFilter 就可以拦截这个请求执行登出操作了,这时它只拦截 HTTP 的 POST 请求,而不拦截 GET 请求。
引用
<<深入浅出Spring Boot 2.x >>
以上是 如何伪装成一个服务端开发SpringSecurity 的全部内容, 来源链接: utcz.com/z/512336.html