Spring Security是一款基于Spring的安全框架,主要包含认证和授权两大安全模块,和另外一款流行的安全框架Apache Shiro相比,它的功能更为强大,并且Shiro其实存在反编译漏洞,可能带来一定安全隐患。
Spring Security还可以轻松的自定义扩展以满足各种需求,对常见的Web安全攻击提供了防护支持。如果你的Web框架选择的是Spring Boot,那么在安全方面Spring Security会是一个不错的选择。因为Spring Boot使用了特定的方式来进行配置,从而使开发人员不再需要定义繁琐的XML配置文件,使用对应注解就可以管理对象的生命周期。
一般来说,常见的安全管理技术栈的组合是这样的:
我们来看下具体使用。
1.创建项目
在 Spring Boot 中使用 Spring Security 非常容易,引入依赖即可:

pom.xml 中的 Spring Security 依赖:
<parent> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-parentartifactId> <version>2.4.0version> <relativePath/> parent><dependencies> <dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-securityartifactId> dependency> <dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-webartifactId> dependency>dependencies>
只要加入依赖,项目的所有接口都会被自动保护起来。
2.启动项目
我们创建一个 HelloController:
@RestControllerpublic class HelloController { @GetMapping("hello") public String hello() { return "hello"; }}
访问 localhost:8080/hello ,被重定向到localhost:8080/login,必须登录之后才能访问。

当用户从浏览器发送请求访问服务器地址(localhost:8080/)时,服务端会返回 302 响应码,让客户端重定向到 /login 页面,用户在 /login 页面登录,登陆成功之后,就会自动跳转到 /hello 接口。
默认情况下,登录的用户名是 user ,密码则是项目启动时随机生成的字符串,可以从启动的控制台日志中看到默认密码:

另外,也可以使用postman来发送请求,使用postman发送请求时,可以将用户信息放在请求头中(这样可以避免重定向到登录页面)。
通过以上两种不同的登录方式,可以看出,Spring Security 支持两种不同的认证方式:
可以通过 form 表单来认证
可以通过 HttpBasic 来认证
我们可以通过一些配置将HTTP Basic认证修改为基于表单的认证方式。
创建一个配置类 BrowserSecurityConfig 继承org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter这个抽象类并重写configure(HttpSecurity http)方法。WebSecurityConfigurerAdapter 是由Spring Security提供的Web应用安全配置的适配器:
@Configurationpublic class BrowserSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { // http.formLogin() // 表单方式 http.httpBasic() // HTTP Basic方式 .and() .authorizeRequests() // 授权配置 .anyRequest() // 所有请求 .authenticated(); // 都需要认证 }}
3.用户名/密码配置
随机生成的密码,每次启动时都会变。对登录的用户名/密码进行配置,有三种不同的方式:
3.1 配置文件配置用户名/密码
可以直接在配置文件 application.yml文件中配置用户的基本信息:
spring: security: user: name: bos password: 123
配置完成后,重启项目,就可以使用配置文件中的用户名/密码登录了。
3.2 Java 配置用户名/密码
也可以在 Java 代码中配置用户名密码,首先需要我们创建一个 Spring Security 的配置类,集成自 WebSecurityConfigurerAdapter 类,如下:
@Configurationpublic class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //下面这两行配置表示在内存中配置了两个用户 auth.inMemoryAuthentication() .withUser("javaboy").roles("admin").password("$2a$10$OR3VSksVAmCzc.7WeaRPR.t0wyCsIj24k0Bne8iKWV1o.V9wsP8Xe") .and() .withUser("lisi").roles("bos").password("$2a$10$p1H8iWa8I4.CA.7Z8bwLjes91ZpY.rYREGHQEInNtAp4NzL6PLKxi"); } @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }}
这里我们在 configure 方法中配置了两个用户,用户的密码都是加密之后的字符串(明文是 123),从 Spring5 开始,强制要求密码要加密,如果非不想加密,可以使用一个过期的 PasswordEncoder 的实例 NoOpPasswordEncoder,但是不建议这么做,毕竟不安全。
Spring Security 中提供了 BCryptPasswordEncoder 密码编码工具,可以非常方便的实现密码的加密加盐,相同明文加密出来的结果总是不同,这样就不需要用户去额外保存盐的字段了,这一点比 Shiro 要方便很多。
3.3 从数据库加载用户名/密码
3.3.1 创建SpringSecurityConfig类
@Configuration@EnableWebSecuritypublic class SpringSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyUserService myUserService; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/authentication/*","/login") // 不需要登录就可以访问 .permitAll() .antMatchers("/user/**").hasAnyRole("USER") // 需要具有ROLE_USER角色才能访问 .antMatchers("/admin/**").hasAnyRole("ADMIN") // 需要具有ROLE_ADMIN角色才能访问 .anyRequest().authenticated() .and() .formLogin() .loginPage("/authentication/login") // 设置登录页面 .loginProcessingUrl("/authentication/form") .defaultSuccessUrl("/user/index"); // 设置默认登录成功后跳转的页面 } // 密码加密方式 @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } // 重写方法,自定义用户 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception {// auth.inMemoryAuthentication().withUser("lzc").password(new BCryptPasswordEncoder().encode("123456")).roles("ADMIN","USER");// auth.inMemoryAuthentication().withUser("zhangsan").password(new BCryptPasswordEncoder().encode("123456")).roles("USER"); auth.userDetailsService(myUserService); // 注入MyUserService,这样SpringSecurity会调用里面的loadUserByUsername(String s) }}
3.3.2 MyUserService
这个类实现了UserDetailsService接口,里面有一个loadUserByUsername(String s)方法,这个方法返回一个UserDetails ,UserDetails 是一个接口,而org.springframework.security.core.userdetails.User实现了UserDetails,因此这里我们可以直接使用SpringSecurity提供的User对象,当然如果不想使用SpringSecurity提供的User对象,我们也可以自己编写一个实现UserDetails接口的对象。
@Componentpublic class MyUserService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { // 省略从数据库中获取用户信息的过程... // 通过用户名s去数据库里查找用户以及用户权限 // 然后返回User对象,注意,这里的User对象是SpringSecurity提供的 return new User(s,new BCryptPasswordEncoder().encode("123456"),AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN,ROLE_USER")); }}
4.自定义用户认证
对于登录接口,登录成功后的响应,登录失败后的响应,我们都可以在 WebSecurityConfigurerAdapter 的实现类中进行配置。例如下面这样:
@Configurationpublic class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired VerifyCodeFilter verifyCodeFilter; //验证码过滤器,继承OncePerRequestFilter,保证过滤器每次请求只会被调用一次 @Override protected void configure(HttpSecurity http) throws Exception { http.addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class); http .authorizeRequests()//开启登录配置 .antMatchers("/hello").hasRole("admin")//表示访问 /hello 这个接口,需要具备 admin 这个角色 .anyRequest().authenticated()//表示剩余的其他接口,登录之后就能访问 .and() .formLogin() //定义登录页面,未登录时,访问一个需要登录之后才能访问的接口,会自动跳转到该页面 .loginPage("/login_p") //登录处理接口 .loginProcessingUrl("/doLogin") //定义登录时,用户名的 key,默认为 username .usernameParameter("uname") //定义登录时,用户密码的 key,默认为 password .passwordParameter("passwd") //登录成功的处理器 .successHandler(new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException { resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); out.write("success"); out.flush(); } }) .failureHandler(new AuthenticationFailureHandler() { @Override public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException exception) throws IOException, ServletException { resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); out.write("fail"); out.flush(); } }) .permitAll()//和表单登录相关的接口统统都直接通过 .and() .logout() .logoutUrl("/logout") .logoutSuccessHandler(new LogoutSuccessHandler() { @Override public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException { resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); out.write("logout success"); out.flush(); } }) .permitAll() .and() .httpBasic() .and() .csrf().disable(); }}
我们可以在 successHandler 方法中,配置登录成功的回调,如果是前后端分离开发的话,登录成功后返回 JSON 即可,同理,failureHandler 方法中配置登录失败的回调,logoutSuccessHandler 中则配置注销成功的回调。
5.忽略拦截
如果某一个请求地址不需要拦截的话,有两种方式实现:
推荐使用第二种方案,配置如下:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/vercode");
}
}
6.基本原理
代码的执行过程可以简化为下图表示:

如上图所示,Spring Security包含了众多的过滤器,这些过滤器形成了一条链,所有请求都必须通过这些过滤器后才能成功访问到资源。其中UsernamePasswordAuthenticationFilter过滤器用于处理基于表单方式的登录认证,而BasicAuthenticationFilter用于处理基于HTTP Basic方式的登录验证,后面还可能包含一系列别的过滤器(可以通过相应配置开启)。在过滤器链的末尾是一个名为FilterSecurityInterceptor的拦截器,用于判断当前请求身份认证是否成功,是否有相应的权限,当身份认证失败或者权限不足的时候便会抛出相应的异常。ExceptionTranslateFilter捕获并处理,所以我们在ExceptionTranslateFilter过滤器用于处理了FilterSecurityInterceptor抛出的异常并进行处理,比如需要身份认证时将请求重定向到相应的认证页面,当认证失败或者权限不足时返回相应的提示信息。
Spring Security 可以结合 OAuth2 ,玩出更多的花样出来