快速入门:
1.引入Spring Security依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
2.在项目中编写一个用于测试的/hello接口:
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(){
return "hello spring security";
}
}
3.测试访问
url: http://localhost:8080/hello
默认的登录用户名是user,登录密码是一个随机生成的UUID字符串,在项目启动日志中可以看到登录密码(项目每次重新启动时都会发生变化)
访问成功:
流程分析:
(1)客户端(浏览器)发起请求去访问/hello接口,这个接口默认是需要认证之后才能访问的。
(2)这个请求会走一遍Spring Security中的过滤器链,在最后的FilterSecurityInterceptor过滤器中被拦截下来,因为系统发现用户未认证。请求拦截下来之后,接下来会抛出AccessDeniedException异常。
(3)抛出的AccessDeniedException异常在ExceptionTranslationFilter过滤器中被捕获,ExceptionTranslationFilter过滤器通过调用LoginUrlAuthenticationEntryPoint的commence方法给客户端返回502,要求客户端重定向到/login页面
(4)客户端发送/login请求。
(5)/login请求被DefaultLoginPageGeneratingFilter过滤器拦截下来,并在该过滤器中返回登录界面。所以用户访问/hello接口时首先看到登录页面。
在整个过程中,相当于客户端一共发送了两个请求,第一个请求是/hello,服务端收到之后,返回302,要求客户端重定向到/login,于是客户端又发送了/login请求。
原理分析:
在上述例子中,SpringBoot涉及到的操作:
(1)开启Spring Security 自动化配置,开启后,会自动创建一个名为springSecurityFilterChain的过滤器,并注入到Spring容器中,这个过滤器将负责所有的安全管理,包括用户的认证、授权、重定向到登录页面等(springSecurityFilterChain实际上代理了SpringSecurity中的过滤链)
(2)创建一个UserDetailsService实例,UserDetailsService负责提供用户数据,默认的用户数据是基于内存的用户,用户名为user,密码则是随机生成的UUID字符串
(3)给用户生成一个默认的登录页面
(4)开启CSRF攻击防御
(5)开启会话固定攻击防御
(6)集成X-XSS-Protection
(7)集成X-Frame-Options以防止单击劫持
默认用户生成:
Spring Security中定义了UserDetails接口来规范开发者自定义的用户对象,这样方便一些旧系统、用户表已经固定的系统集成到Spring Security认证体系中。
UserDetails接口定义:
public interface UserDetails extends Serializable{
//获取用户的权限
Collection<? extends GrantedAuthority> getAuthorities();
//返回当前账户的密码
String getPassword();
//返回当前账户的用户名
String getUsername();
//返回当前账户是否未过期
boolean isAccountNonExpired();
//返回当前账户是否未锁定
boolean isAccountNonLocked();
//返回当前账户凭证(如密码)是否未过期
boolean isCredentialsNonExpired();
//返回当前账户是否可用
boolean isEnabled();
}
UserDetailsService(提供用户数据源)接口定义:
public interface UserDetailsService{
//这里的参数username,不一定是通过登录表单输入的用户名,在CAS单点登录时,这里是CAS Server认证成功后回调的用户名参数
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
UserDetailsService的几个实现类和接口:
注:如果仅仅只引入Spring Security依赖,没有配置的情况下默认使用的用户是InMemoryUserDetailsManager提供的
(1)UserDetailsManager接口: 在UserDetails的基础上,继续定义了添加、更新、删除用户、修改密码以及判断用户是否存在等五种方法
(2)JdbcDaoImpl实现类:在UserDetails的基础上,通过spring-jdbc实现了从数据库中查询用户的方法
(3)InMemoryUserDetailsManager实现类:实现了UserDetailsManager中关于用户的增删查改方法,不过都是基于内存的操作,数据并没有持久化
(4)JdbcUserDetailsManager实现类:继承自JdbcDaoImpl同时又实现了UserDetailsMananger接口,可以实现对用户的增删查改操作,并且都会持久化到数据库中,但由于操作数据库的SQL都是提前写好的,不够灵活,因此实际开发中该类使用的并不多
(5)CachingUserDetailsService实现类:特点是会将UserDetailsService缓存起来
(6)UserDetailsServiceDelegator实现类:提供了UserDetailsService的懒加载功能
(7)ReactiveUserDetailsServiceAdapter:是webflux-web-security模块定义的UserDetailsService实现
UserDetailsService的自动配置类——UserDetailsServiceAutoConfiguration类:
什么情况下会进行自动配置?
(1)当前classpath下存在AuthenticationManager类
(2)当前项目中,系统没有提供AuthenticationManager、AuthenticationProvider、UserDetailsService以及ClientRegistrationRepository实例
自动配置提供的是InMemoryUserDetailsManager实例,而该实例中的用户数据源自SecurityProperties类的getUser()方法:
@ConfigurationProperties(prefix="spring.security")
public class SecurityProperties{
//org.springframework.security.core.userdetails.User
//这是Spring Security提供的一个实现了UserDetails接口的用户类
private User user = new User();
public User getUser(){
return this.user;
}
public static class User{
private String name = "user";
private String password = UUID.randomUUID().toString();
private List<String> roles = new ArrayList<>();
//省略getter/setter
}
}
默认的用户密码在getOrDeducePassword方法中进行了二次处理,默认的encoder为null,所以密码的二次处理只是给密码加了一个前缀{noop},表示密码是明文存储的。
在项目的配置文件中添加如下内容,便可以实现定制SecurityProperties.User中的属性值:
spring:
security:
user:
name: lalala # 默认的登录使用的用户名
password: 123 # 密码
roles: admin,user # 权限
默认页面生成:
上述例子中,存在着两个默认页面,及其对应的用于生成页面的过滤器:
登录页面:localhost:8080/login ——> DefaultLoginPageGeneratingFilter
注销页面:localhost:8080/logout ——> DefaultLogoutPageGeneratingFilter
DefaultLoginPageGeneratingFilter的执行流程:
(1)在doFilter()中判断,是否是以下三种情况之一:
· 是登录的请求
· 登录失败
· 登出成功
(2)符合以上其中一种情况,则执行generateLoginPageHtml()构建登录页面
(3)将生成的登录页面前端代码通过HttpServletResponse返回给前端
登录表单配置:
自定义配置:
实现自定义配置主要是通过继承WebSecurityConfigurerAdapter来实现的:
简化的配置示例(省略自定义登录页面和配置文件中定制的用户名密码):
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/doLogin")
.defaultSuccessUrl("/index")
.failureUrl("/login.html")
.usernameParameter("uname")
.passwordParameter("passwd")
.permitAll()
.and()
.csrf()
.disable();
}
}
其中defaultSuccessUrl和successForwardUrl区别如下:
defaultSuccessUrl()是客户端跳转,而successForwardUrl()则是通过服务器端跳转来实现的,但最终所配置的都是AuthenticationSuccessHandler接口的实例
服务端跳转的其中一个好处是可以携带登录的信息(成功、失败或者异常信息)
(1)defaultSuccessUrl()方法表示当前用户登陆成功后,会自动重定向到登录前访问的地址上,如果用户登录前访问的就是登录页面,则登陆成功后会重定向到defaultSuccessUrl()方法参数中设置的页面。defaultSuccessUrl()有一个重载方法,第二个参数传true的话则效果与successForwardUrl()一致
(2)successForwardUrl()方法则不会考虑用户之前的访问地址,只要用户登录成功,就会通过服务器端跳转到successForwardUrl()参数所指定的页面
AuthenticationSuccessHandler接口:
public interface AuthenticationSuccessHandler{
default void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authentication)
throws IOException, ServletException{
onAuthenticationSuccess(request, response, authentication);
chain.doFilter(request, response);
}
void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication)
throws IOException, ServletException;
}
(1)第一个default方法: 在处理特定的认证请求Authentication Filter中会用到
(2)第二个非default方法: 用来处理登录成功的具体事项,其中authentication参数保存了登录成功的用户信息
该接口的三个实现类:
(1)SimpleUrlAuthenticationSuccessHandler: 继承自AbstractAuthenticationTargetUrlRequestHandler,通过其中的handle方法实现请求重定向
(2)SavedRequestAwareAuthenticationSuccessHandler:在SimpleUrlAuthenticationSuccessHandler的基础上增加了请求缓存的功能,可以记录之前请求的地址,进而在登陆成功后重定向到一开始访问的地址,使用defaultSuccessUrl()对应的实现类就是此类
(3)ForwardAuthenticationSuccessHandler:实现就是一个服务端跳转
前后端分离的情况下,可以通过自定义AuthenticationSuccessHandler来返回登录成功的JSON数据。同样的,登录失败也可以自定义AuthenticationFailureHandler来返回登录失败的JSON数据。
AuthenticationFailureHandler接口:
public interface AuthenticationFailureHandler{
void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception)
throws IOException, ServletException;
}
该接口的五个实现类:
(1)SimpleUrlAuthenticationFailureHandler: 默认的处理逻辑是通过重定向跳转到登录页面,也可以通过配置forwardToDestination属性将重定向改为服务器端跳转,failureUrl方法的底层实现逻辑就是SimpleUrlAuthenticationFailureHandler。
(2)ExceptionMappingAuthenticationFailureHandler: 可以实现根据不同的异常类型,映射到不同的路径。
(3)ForwardAuthenticationFailureHandler:表示通过服务器端跳转来重新回到登录页面,failureForwardUrl方法的底层实现就是ForwardAuthenticationFailureHandler。
(4)AuthenticationEntryPointFailureHandler:Spring Security5.2新引进的处理类,可以通过AuthenticationEntryPoint来处理登录异常。
(5)DelegatingAuthenticationFailureHandler:可以实现为不同的异常类型配置不同的登录失败处理回调。
不使用failureForwardUrl方法,又想在登陆失败后通过服务端跳转回到登录页面,可以自定义SimpleUrlAuthenticationFailureHandler配置,并将forwardToDestination属性设置为true。
自定义AuthenticationFailureHandler写法(一部分代码):
.failureHandler((request,
response,
exception) ->{
response.setContentType("application/json;charset=utf-8");
Map<String,Object> resp = new HashMap<>();
resp.put("status", 500);
resp.put("msg", "登录失败!" + exception.getMessage());
String msg = new ObjectMapper().writeValueAsString(resp);
response.getWriter().write(msg);
})
注销登录:
常用的配置:
(1)通过.logout()方法开启注销登录配置。
(2)logoutUrl指定了注销登录请求地址,默认是GET请求,路径为/logout
(3)invalidateHttpSession:表示是否使session失效,默认为true
(4)clearAuthentication:表示是否清除认证信息,默认为true
(5)logoutSuccessUrl:表示注销登录后的跳转地址。
前后端分离,注销成功后就不需要页面跳转了,可以通过配置.logoutSuccessHandler(),自定义LogoutSuccessHandler来返回JSON数据,方式和上面AuthenticationFailureHandler的一致。通过配置.defaultLogoutSuccessHandlerFor()(可以配置多个),可以注册多个不同的注销成功回调函数,例:
.defaultLogoutSuccessHandlerFor((req,resp,auth)->{
resp.setContentType("application/json;charset=utf-8");
Map<String,Object> result = new HashMap<>();
result.put("status", 200);
result.put("msg", "注销成功!");
String msg = new ObjectMapper().writeValueAsString(resp);
resp.getWriter().write(msg);
}, new AntPathRequestMatcher("/logout", "GET"))
.defaultLogoutSuccessHandlerFor((req,resp,auth)->{
resp.setContentType("application/json;charset=utf-8");
Map<String,Object> result = new HashMap<>();
result.put("status", 200);
result.put("msg", "注销成功!");
String msg = new ObjectMapper().writeValueAsString(resp);
resp.getWriter().write(msg);
}, new AntPathRequestMatcher("/logout01", "GET"))