|
| 1 | +# 用户名密码登录解析 |
| 2 | + |
| 3 | +## 相关代码位置 |
| 4 | + |
| 5 | +相关代码都位于 `API` 模块下的 |
| 6 | + |
| 7 | +- 【package】com.databasir.api.config.security.* |
| 8 | +- 【class】com.databasir.api.config.SecurityConfig.java |
| 9 | + |
| 10 | + |
| 11 | + |
| 12 | +## 登录认证过滤器 |
| 13 | + |
| 14 | +Databasir 的登录认证都是在 Filter 中实现的,目前有两个自定义的 Filter |
| 15 | + |
| 16 | +- `DatabasirOauth2LoginFilter`:负责处理 OAuth2 登录的请求 |
| 17 | +- `DatabasirJwtTokenFilter`:负责验证登录凭证的有效性 |
| 18 | + |
| 19 | +用户名和密码的登录采用 Spring Security 自带的 Filter |
| 20 | + |
| 21 | +- `UsernamePasswordAuthenticationFilter`:负责处理用户名密码登录的请求 |
| 22 | + |
| 23 | +用户登录请求都会经过这三个过滤器 |
| 24 | + |
| 25 | + |
| 26 | + |
| 27 | +由于本篇主要说明用户明密码登录这一流程,所以会重点专注于 `UsernamePasswordFilter` 这一过滤器。 |
| 28 | + |
| 29 | +## 登录认证相关类 |
| 30 | + |
| 31 | +用户名密码登录是基于 Spring Security 提供的 `UsernamePasswordAuthenticationFilter` 实现。 |
| 32 | + |
| 33 | +Databasir 根据该过滤器的扩展点自定义了以下类 |
| 34 | + |
| 35 | +- DatabasirAuthenticationFailureHandler:登录失败回调 |
| 36 | +- DatabasirAuthenticationSuccessHandler:登录成功回调 |
| 37 | +- DatabasirUserDetailService:获取用户登录信息的 service |
| 38 | +- DatabasirUserDetails:用户的登录信息(扩展了角色信息) |
| 39 | + |
| 40 | +这些类的装配都在 `com.databasir.api.config.SecurityConfig.java` 中,下面展示了一部分源码,重点关注 `configure(HttpSecurity)` 方法 |
| 41 | + |
| 42 | +```java |
| 43 | +@Configuration |
| 44 | +@RequiredArgsConstructor |
| 45 | +@EnableGlobalMethodSecurity(prePostEnabled = true) |
| 46 | +public class SecurityConfig extends WebSecurityConfigurerAdapter { |
| 47 | + |
| 48 | + private final DatabasirUserDetailService databasirUserDetailService; |
| 49 | + |
| 50 | + private final DatabasirAuthenticationEntryPoint databasirAuthenticationEntryPoint; |
| 51 | + |
| 52 | + private final DatabasirJwtTokenFilter databasirJwtTokenFilter; |
| 53 | + |
| 54 | + private final DatabasirAuthenticationFailureHandler databasirAuthenticationFailureHandler; |
| 55 | + |
| 56 | + private final DatabasirAuthenticationSuccessHandler databasirAuthenticationSuccessHandler; |
| 57 | + |
| 58 | + @Override |
| 59 | + protected void configure(HttpSecurity http) throws Exception { |
| 60 | + http.headers().frameOptions().disable(); |
| 61 | + http.csrf().disable(); |
| 62 | + http.cors(); |
| 63 | + |
| 64 | + http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) |
| 65 | + .and() |
| 66 | + // 启用 form 表单登录方式,配置登录地址为 /login,并制定成功和失败的回调处理类 |
| 67 | + .formLogin() |
| 68 | + .loginProcessingUrl("/login") |
| 69 | + .failureHandler(databasirAuthenticationFailureHandler) |
| 70 | + .successHandler(databasirAuthenticationSuccessHandler) |
| 71 | + .and() |
| 72 | + .authorizeRequests() |
| 73 | + // 登录和 Token 刷新无需授权 |
| 74 | + .antMatchers("/login", Routes.Login.REFRESH_ACCESS_TOKEN) |
| 75 | + .permitAll() |
| 76 | + // oauth 回调地址无需鉴权 |
| 77 | + .antMatchers("/oauth2/apps", "/oauth2/authorization/*", "/oauth2/login/*") |
| 78 | + .permitAll() |
| 79 | + // 静态资源无需鉴权 |
| 80 | + .antMatchers("/", "/*.html", "/js/**", "/css/**", "/img/**", "/*.ico") |
| 81 | + .permitAll() |
| 82 | + // api 请求需要授权 |
| 83 | + .antMatchers("/api/**").authenticated() |
| 84 | + .and() |
| 85 | + .exceptionHandling() |
| 86 | + .authenticationEntryPoint(databasirAuthenticationEntryPoint); |
| 87 | + |
| 88 | + http.addFilterBefore( |
| 89 | + databasirJwtTokenFilter, |
| 90 | + UsernamePasswordAuthenticationFilter.class |
| 91 | + ); |
| 92 | + } |
| 93 | + |
| 94 | + @Override |
| 95 | + protected void configure(AuthenticationManagerBuilder auth) throws Exception { |
| 96 | + // 这里指定了 userDetailService 为自定义的 DatabasirUserDetailService |
| 97 | + auth.userDetailsService(databasirUserDetailService) |
| 98 | + .passwordEncoder(bCryptPasswordEncoder()); |
| 99 | + } |
| 100 | + |
| 101 | +} |
| 102 | +``` |
| 103 | + |
| 104 | + |
| 105 | + |
| 106 | +## 登录认证过滤器处理流程 |
| 107 | + |
| 108 | +当请求经过 `UsernamePasswordAuthenticationFilter` 时,该过滤器会校验请求的路径是否是 `/login` ,如果不是的话就直接放行。 |
| 109 | + |
| 110 | +然后在从请求参数里面获取用户名和密码 |
| 111 | + |
| 112 | +- username |
| 113 | +- password |
| 114 | + |
| 115 | +再之后使用 `DatabasirUserDetailsService` 根据用户名获取实际的用户信息,将实际的密码和登录时传的参数做比较 |
| 116 | + |
| 117 | +- 如果不一致就登录失败,调用 `DatabasirAuthenticationFailureHandler` |
| 118 | +- 如果一致就说明登录成功,调用 `DatabasirAuthenticationSuccessHandler` |
| 119 | + |
| 120 | +下图展示了一个简化的代码调用时序 |
| 121 | + |
| 122 | + |
| 123 | + |
| 124 | + |
| 125 | + |
| 126 | +## 登录成功回调 |
| 127 | + |
| 128 | +当用户名密码认证通过以后就会触发**登录成功回调**(代码实现在`DatabasirAuthenticationSuccessHandler`),这里主要做一件事情 |
| 129 | + |
| 130 | +- 生成登录凭证:access_token、refresh_token |
| 131 | + |
| 132 | +access_token 是请求接口的凭证,证明你是合法的登录用户,时效性是以分钟为单位,格式为 JWT。 |
| 133 | + |
| 134 | +refresh_token 是用于在 access_token 过期时,用于获取新的 access_token,时效性是以天为单位。 |
| 135 | + |
| 136 | +这些信息都存储在 login 表里,login 表与 user 表是一对一的关系 |
| 137 | + |
| 138 | + |
| 139 | + |
| 140 | +access_token 和 refresh_token 最终都会返回给前端,前端拿到 token 以后调用业务接口都需在请求中加入 名为 Authorization 的 Header,值为 access_token |
| 141 | + |
| 142 | +```http |
| 143 | +POST /api/v1.0/groups |
| 144 | +
|
| 145 | +Authorization: {{ access_token }} |
| 146 | +``` |
| 147 | + |
| 148 | +如果 access_token 过期,前端会拿 refresh_token 获取一个新的 access_token,然后再调用业务接口,这些过程对用户是透明的。 |
| 149 | + |
| 150 | + |
| 151 | + |
| 152 | +## 登录失败回调 |
| 153 | + |
| 154 | +当用户名密码认证没通过的时候机会触发登录失败回调,源码位于 `DatabasirAuthenticationFailureHandler` 中。 |
| 155 | + |
| 156 | +失败的回调会判断异常类型做出不同的响应 |
| 157 | + |
| 158 | +- BadCredentialsException:响应 200,用户名或密码错误 |
| 159 | +- DisabledException:响应 200,用户已禁用 |
| 160 | +- DatabasirAuthenticationException:响应 200,自定义登录异常 |
| 161 | +- 其他:响应 401 |
0 commit comments