如何伪装成一个服务端开发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,密码随机的用户。

内存签名服务

在项目中新增如下文件

@Configuration

public 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 定义一个服务

@Service

public 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进行配置,比如。

@Override

protected 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)功能。

@Override

protected 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)

@Override

protected 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

回到顶部