意识 Spring Security
Spring Security 是为基于 Spring 的应用程序提供申明式平安爱护的安全性框架。Spring Security 提供了残缺的安全性解决方案,它可能在 Web 申请级别和办法调用级别解决身份认证和受权。因为基于 Spring 框架,所以 Spring Security 充分利用了依赖注入(dependency injection,DI)和面向切面的技术。
外围性能
对于一个权限治理框架而言,无论是 Shiro 还是 Spring Security,最最外围的性能,无非就是两方面:
认证
受权
艰深点说,认证就是咱们常说的登录,受权就是权限甄别,看看申请是否具备相应的权限。
认证(Authentication)
Spring Security 反对多种不同的认证形式,这些认证形式有的是 Spring Security 本人提供的认证性能,有的是第三方规范组织制订的,次要有如下一些:
一些比拟常见的认证形式:
HTTP BASIC authentication headers:基于 IETF RFC 规范。
HTTP Digest authentication headers:基于 IETF RFC 规范。
HTTP X.509 client certificate exchange:基于 IETF RFC 规范。
LDAP:跨平台身份验证。
Form-based authentication:基于表单的身份验证。
Run-as authentication:用户用户长期以某一个身份登录。
OpenID authentication:去中心化认证。
除了这些常见的认证形式之外,一些比拟冷门的认证形式,Spring Security 也提供了反对。
Jasig Central Authentication Service:单点登录。
Automatic “remember-me” authentication:记住我登录(容许一些非敏感操作)。
Anonymous authentication:匿名登录。
……
作为一个凋谢的平台,Spring Security 提供的认证机制不仅仅是下面这些。如果下面这些认证机制仍然无奈满足你的需要,咱们也能够本人定制认证逻辑。当咱们须要和一些“老破旧”的零碎进行集成时,自定义认证逻辑就显得十分重要了。
受权(Authorization)
无论采纳了下面哪种认证形式,都不影响在 Spring Security 中应用受权性能。Spring Security 反对基于 URL 的申请受权、反对办法拜访受权、反对 SpEL 访问控制、反对域对象平安(ACL),同时也反对动静权限配置、反对 RBAC 权限模型等,总之,咱们常见的权限治理需要,Spring Security 基本上都是反对的。
我的项目实际
创立 maven 工程
我的项目依赖如下:
<dependencies>
<!– 以下是 >spring boot 依赖 –>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!– 以下是 >spring security 依赖 –>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
复制代码
提供一个简略的测试接口,如下:
@RestController
public class HelloController {
@GetMapping(“/hello”)
public String hello() {
return "hello,hresh";
}
@GetMapping(“/hresh”)
public String sayHello() {
return "hello,world";
}
}
复制代码
再创立一个启动类,如下:
@SpringBootApplication
public class SecurityInMemoryApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityInMemoryApplication.class, args);
}
}
复制代码
在 Spring Security 中,默认状况下,只有增加了依赖,咱们我的项目的所有接口就曾经被通通爱护起来了,当初启动我的项目,拜访 /hello 接口,就须要登录之后才能够拜访,登录的用户名是 user,明码则是随机生成的,在我的项目的启动日志中,如下所示:
Using generated security password: 21596f81-e185-4b6a-a8ff-1b21e2a60c6f
复制代码
咱们尝试拜访 /hello 接口,因为该接口被 Spring Security 爱护起来了,重定向到 /login 接口,如下图所示:
输出账号和明码后,即可拜访 /hello 接口。
那么如何自定义登录用户信息呢?以及 Spring Security 如何晓得咱们想要反对基于表单的身份验证?
认证
启用 web 安全性性能
Spring Security 提供了用户名明码登录、退出、会话治理等认证性能,只须要配置即可应用。
在 Spring Security 5.7 版本之前,或者 SpringBoot2.7 之前,咱们都是继承 WebSecurityConfigurerAdapter 来配置。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 定义用户信息服务(查问用户信息)
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
return manager;
}
// 明码编码器, 不加密,字符串间接比拟
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/hello");
}
// 平安拦挡机制(最重要)
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.httpBasic();
}
}
复制代码
Spring Security 提供了这种链式的办法调用。下面配置指定了认证形式为 HTTP Basic 登录,并且所有申请都须要进行认证。
这里有一点须要留神,我没并没有在 Spring Security 配置类上应用 @EnableWebSecurity 注解。这是因为在非 Spring Boot 的 Spring Web MVC 利用中,注解 @EnableWebSecurity 须要开发人员本人引入以启用 Web 平安。而在基于 Spring Boot 的 Spring Web MVC 利用中, 开发人员没有必要再次援用该注解,Spring Boot 的主动配置机制 WebSecurityEnablerConfiguration 曾经引入了该注解,如下所示:
package org.springframework.boot.autoconfigure.security.servlet;
// 省略 imports 行
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnMissingBean(
name = {“springSecurityFilterChain”}
)
@ConditionalOnClass({EnableWebSecurity.class})
@ConditionalOnWebApplication(
type = Type.SERVLET
)
@EnableWebSecurity
class WebSecurityEnablerConfiguration {
WebSecurityEnablerConfiguration() {
}
}
复制代码
实际上,一个 Spring Web 利用中,WebSecurityConfigurerAdapter 可能有多个 , @EnableWebSecurity 能够不必在任何一个 WebSecurityConfigurerAdapter 上,能够用在每个 WebSecurityConfigurerAdapter 上,也能够只用在某一个 WebSecurityConfigurerAdapter 上。多处应用 @EnableWebSecurity 注解并不会导致问题,其最终运行时成果跟应用 @EnableWebSecurity 一次成果是一样的。
在 userDetailsService()办法中,咱们返回了一个 UserDetailsService 给 Spring 容器,Spring Security 会应用它来获取用户信息。咱们临时应用 InMemoryUserDetailsManager 实现类,并在其中别离创立了 zhangsan、lisi 两个用户,并设置明码和权限。
在 configure(HttpSecurity http)办法中进入如下配置:
确保对咱们的应用程序的任何申请都要求用户进行身份验证
容许用户应用基于表单的登录进行身份验证
容许用户应用 HTTP 根本身份验证进行身份验证
留神上述还有一个 passwordEncoder()办法,在 IDEA 中会提醒 NoOpPasswordEncoder 已过期。这是因为 Spring Security 5 对 PasswordEncoder 做了相干的重构,原先默认配置的 PlainTextPasswordEncoder(明文明码)被移除了,想要做到明文存储明码,只能应用一个过期的类来过渡。
// 退出
// 已过期
@Bean
PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
复制代码
Spring Security 提供了多品种来进行明码编码,并作为了相干配置的默认配置,只不过没有裸露为全局的 Bean。在理论利用中应用明文校验明码必定是存在危险的,NoOpPasswordEncoder 只能存在于 demo 中。
// 理论利用
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
// 加密形式与对应的类
bcrypt – BCryptPasswordEncoder (Also used for encoding)
ldap – LdapShaPasswordEncoder
MD4 – Md4PasswordEncoder
MD5 – new MessageDigestPasswordEncoder(“MD5”)
noop – NoOpPasswordEncoder
pbkdf2 – Pbkdf2PasswordEncoder
scrypt – SCryptPasswordEncoder
SHA-1 – new MessageDigestPasswordEncoder(“SHA-1”)
SHA-256 – new MessageDigestPasswordEncoder(“SHA-256”)
sha256 – StandardPasswordEncoder
复制代码
然而在 Spring Security 5.7 版本之后(包含 5.7 版本),或者 SpringBoot2.7 之后,WebSecurityConfigurerAdapter 就过期了,尽管能够持续应用,但看着比拟顺当。
看 5.7 版本官网文档是如何解释的:
以前咱们自定义类继承自 WebSecurityConfigurerAdapter 来配置咱们的 Spring Security,咱们次要是配置两个货色:
configure(HttpSecurity)
configure(WebSecurity)
前者次要是配置 Spring Security 中的过滤器链,后者则次要是配置一些门路放行规定。
当初在 WebSecurityConfigurerAdapter 的正文中,人家曾经把意思说的很明确了:
当前如果想要配置过滤器链,能够通过自定义 SecurityFilterChain Bean 来实现。
当前如果想要配置 WebSecurity,能够通过 WebSecurityCustomizer Bean 来实现。
咱们对上文中的 SecurityConfig 文件做一下改变,试试新版中该如何配置。
@Configuration
public class SecurityConfig {
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
return manager;
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Bean
WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring().antMatchers("/hello");
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.permitAll()
.and()
.csrf().disable();
return http.build();
}
}
复制代码
此时重启我的项目,你会发现 /hello 也是能够间接拜访的,就是因为这个门路不通过任何过滤器。
集体感觉新写法更加直观,能够分明的看到 SecurityFilterChain 是对于过滤器链配置的,与咱们理论知识提到的过滤器常识是统一的。
测试
拜访 http://localhost:8086/hello,能够间接看到页面内容,不须要输出账号密码。
拜访 http://localhost:8086/hresh,则须要账号密码,即咱们配置的 zhangsan 和 lisi 用户。
在测试过程中,你可能会发现这样几个问题:
1、间接拜访 http://localhost:8086,默认会跳转到 /login 页面,该配置位于 UsernamePasswordAuthenticationFilter 类文件中,如果你想自定义登录页面,能够这样批改:
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
// .loginPage(“/login.html”)
.loginProcessingUrl("/login")
复制代码
2、表单登录时,账号密码默认字段为 username 和 password。
3、按理来说,登录胜利之后是跳到 / 页面,失败跳转到登录页,但因为咱们这是 SpringBoot 我的项目,咱们能够让它登录胜利时返回 json 数据,而不是重定向到某个页面。默认状况下,账号密码输出谬误会主动返回登录页面,所以此处咱们就不解决失败的状况。
@Component
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
private static ObjectMapper objectMapper = new ObjectMapper();
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {response.setContentType("application/json;charset=utf-8");
response.getWriter().write(objectMapper.writeValueAsString("登录胜利"));
}
}
复制代码
接着批改 securityFilterChain()办法
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.successHandler(myAuthenticationSuccessHandler)
.and()
.csrf().disable();
复制代码
再次重启我的项目,在登录页面输出账号密码后,返回后果如下所示:
4、自定义登录页面,在 resource 目录下新建 static 目录,外面增加 login.html 文件,临时未增加款式
<!DOCTYPE html>
<html>
<head>
<meta charset=”UTF-8″>
<title> 登录 </title>
</head>
<body>
<form action=”/doLogin” method=”post”>
<div class=”input”>
<label for="name"> 用户名 </label>
<input type="text" name="name" id="name">
<span class="spin"></span>
</div>
<div class=”input”>
<label for="pass"> 明码 </label>
<input type="password" name="passwd" id="pass">
<span class="spin"></span>
</div>
<div class=”button login”>
<button type="submit">
<span> 登录 </span>
<i class="fa fa-check"></i>
</button>
</div>
</form>
</body>
</html>
复制代码
批改 securityFilterChain()办法
http.authorizeRequests() // 示意开启权限配置
.antMatchers(“/login.html”).permitAll()
.anyRequest().authenticated() // 示意所有的申请都要通过认证之后能力拜访
.and() // 链式编程写法
.formLogin() // 开启表单登录配置
.loginPage(“/login.html”) // 配置登录页面地址
.loginProcessingUrl(“/doLogin”)
.permitAll()
.and()
.csrf().disable();
复制代码
重启我的项目后,再次拜访 http://localhost:8086/,会重定向到 http://localhost:8086/login.html。
最初,总结一下 HttpSecurity 的配置,示例如下:
http.authorizeRequests() // 示意开启权限配置
.anyRequest().authenticated() // 示意所有的申请都要通过认证之后能力拜访
.and() // 链式编程写法
.formLogin() // 开启表单登录配置
.loginPage("/login.html") // 配置自定义登录页面地址
.loginProcessingUrl("/login") // 配置登录接口地址
// .defaultSuccessUrl() // 登录胜利后的跳转页面
// .failureUrl() // 登录失败后的跳转页面
// .usernameParameter(“username”) // 登录用户名的参数名称
// .passwordParameter(“password”) // 登录明码的参数名称
// .successHandler(
// myAuthenticationSuccessHandler) // 前后端拆散的状况,并不想通过 defaultSuccessUrl 进行页面跳转,只须要返回一个 json 数据来告知前端
// .failureHandler(myAuthenticationFailureHandler) // 同理,代替 failureUrl
// .permitAll()
.and()
.csrf().disable();// 禁用 CSRF 进攻性能,测试能够先敞开
复制代码
表单验证时,loginPage 与 loginProcessingUrl 区别:
loginPage 配置自定义登录页面地址,loginProcessingUrl 默认与表单 action 地址统一;
如果只配置 loginPage 而不配置 loginProcessingUrl,那么 loginProcessingUrl 默认就是 loginPage;
如果只配置 loginProcessUrl,就会用不了自定义登陆页面,Security 会应用自带的默认登陆页面;
如果 loginProcessingUrl 默认与表单 action 地址不统一,那么它须要指向一个无效的地址,比如说 /doLogin.html,这要求咱们在 static 目录下创立一个 doLogin.html 页面,此外,还须要在 controller 文件中减少如下办法:
@PostMapping(“/doLogin”)
public String doLogin() {
return "我登录胜利了";
}
复制代码
然而登录胜利后并不会显示 doLogin.html 页面的内容,而是显示 /doLogin 的返回后果。同理,不配置 loginProcessingUrl,那么 loginProcessingUrl 默认就是 loginPage,即 loginProcessingUrl=login.html,与 doLogin.html 成果一样。
另外再介绍一下 Spring Security 中 defaultSuccessUrl 和 successForwardUrl 的区别:
假设在 defaultSuccessUrl 中指定登录胜利的跳转页面为 /index,那么存在两种状况:
① 浏览器中输出的是登录地址,登录胜利后,则间接跳转到 /index;
② 如果浏览器中输出了其余地址,例如 http://localhost:8080/elseUrl,若登录胜利,就不会跳转到 /index,而是来到 /elseUrl 页面。
defaultSuccessUrl 就是说,它会默认跳转到 Referer 起源页面,如果 Referer 为空,没有起源页,则跳转到默认设置的页面。
successForwardUrl 示意不论 Referer 从何而来,登录胜利后一律跳转到指定的地址。
认证形式抉择
在 WebSecurityConfigurerAdapter 类中有很多 configure()办法,除了上文提到的 HttpSecurity 和 WebSecurity 参数,还有一个 AuthenticationManagerBuilder 参数,源码如下:
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
this.disableLocalConfigureAuthenticationBldr = true;
}
protected AuthenticationManager authenticationManager() throws Exception {
if (!this.authenticationManagerInitialized) {
this.configure(this.localConfigureAuthenticationBldr);
if (this.disableLocalConfigureAuthenticationBldr) {this.authenticationManager = this.authenticationConfiguration.getAuthenticationManager();
} else {this.authenticationManager = (AuthenticationManager)this.localConfigureAuthenticationBldr.build();}
this.authenticationManagerInitialized = true;
}
return this.authenticationManager;
}
复制代码
该类用于设置各种用户想用的认证形式,设置用户认证数据库查问服务 UserDetailsService 类以及增加自定义 AuthenticationProvider 类实例等
Spring Security 为配置用户存储提供了多个可选解决方案,包含:
基于内存的用户存储
基于 JDBC 的用户存储
以 LDAP 作为后端的用户存储
自定义用户详情服务
对于这四种形式就不具体介绍了,能够重点关注计划二和计划四,而在本我的项目中,咱们间接在 SecurityConfig 中重写 userDetailsService 办法,并将 UserDetailsService 对象注入到 Spring 容器中。
受权
1、首先在 HelloController 中减少 r1 和 r2 资源。
@GetMapping(value = “/r/r1”)
public String r1() {
return ” 拜访资源 1 ″;
}
@GetMapping(value = “/r/r2”)
public String r2() {
return ” 拜访资源 2 ″;
}
复制代码
2、批改 SecurityConfig 文件中的 securityFilterChain() 办法
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/r/r1").hasAuthority("p1")
.antMatchers("/r/r2").hasAuthority("p2")
.anyRequest().authenticated()
.and()
.formLogin()
.successHandler(myAuthenticationSuccessHandler)
.permitAll()
.and()
.csrf().disable();
return http.build();
}
复制代码
拜访 r1、r2 资源,须要对应的权限,而且其余接口则只须要认证,并不需要受权。
3、测试
拜访 http://localhost:8086,进入登录页面,输出正确的账号密码,提交后页面返回“登录胜利”,如果是 zhangsan,则能够拜访 r1 资源,拜访 r2 则会报错,咱们临时未处理错误如下:
总结
对于 Spring Security 的学习先到这里,根本理解如何应用即可,咱们持续前面的学习。
如果想要深刻学习 Spring Security,举荐浏览《深入浅出 Spring Security》,还包含配套的代码示例。