示例项目地址: https://git.code.tencent.com/xinzhang0618/oa2.git
参考文档:
springSecurity官方文档:
https://docs.spring.io/spring-security/site/docs/5.4.2/reference/html5/#servlet-hello-auto-configuration
springSecurity概念: https://www.jianshu.com/p/7b87ec108405
openssl生成RSA: https://blog.csdn.net/asd54090/article/details/103665966

基础回顾

SpringSecurity框架入门要先了解各种概念, 详情参考上面第二篇文档, 简单总结:
**

认证流程

  1. 请求经过xxxFilter
  2. filter抽取request封装成某一类型的token交于AuthenticationManager进行校验
  3. AuthenticationManager的ProviderManager实现, 根据token类型, 找到具体的xxxProvider进行校验, 核心伪代码如下

    1. public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
    2. public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    3. Iterator var8 = this.getProviders().iterator();
    4. while(var8.hasNext()) {
    5. AuthenticationProvider provider = (AuthenticationProvider)var8.next();
    6. if (provider.supports(toTest)) {
    7. result = provider.authenticate(authentication);
    8. ...
    9. }
    10. }
    11. }
  4. 只要有一个provider验证成功则认证成功

单体认证 - 图1

单体认证 - 图2

相关概念

这里只介绍与本文相关的, 具体参见官方文档

  • 过滤器

    • AbstractAuthenticationProcessingFilter, 所有过滤器的基过滤器
    • UsernamePasswordAuthenticationFilter, 继承上一个, 处理表单登录, 可以看到这里定死了登录方式POST, 路径/login, 如果需要更改这些, 则自定义的Filter直接继承上一个
    • BasicAuthenticationFilter, 处理Basic认证, 本文实现中重写了这个作为token过滤器, 实际上token过滤也可以直接使用spring的OncePerRequestFilter等
  • 校验器, AuthenticationManager定义了校验的顶层接口, 其中ProviderManager作为主要实现, 其中维护了providers列表, 负责对token找到对应的provider进行校验, 比如上图的DaoAuthenticationProvider支持UsernamePasswordAuthenticationToken(及其子类型)的校验

    • 自定义校验器需要实现AuthenticationProvider接口
  • token, springSecurity在过滤器中会将用户信息封装成各个类型的token

    • 自定义的token需要实现AbstractAuthenticationToken接口
  • 其他

    • SecurityContextHolder, 持有SecurityContext上下文信息
    • Authentication, 鉴权对象, 包含principal当事人即当前用户, credentials凭证一般指密码, authorities用户权限

实现思路

做认证主要是登录, 访问刷新, token过期, 登出

  1. 登录, 校验用户名密码, 生成token存redis;
  2. 访问刷新, redis的token续期
  3. token过期, redis的token过期
  4. 登出, 清除redis的token

配置解析

整个配置跟业务关联非常低, 不同项目基本可以直接移植

依赖以及配置文件

  1. <!--springSecurity-->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-security</artifactId>
  5. </dependency>

这里前后端密码传递使用Openssl生成的秘钥对进行加解密的, 数据库存储的密码是md5加密的, 后端只需要存私钥, 公钥存前端

  1. 前端用户登陆时, 使用公钥对密码加密给到后端
  2. 后端拿私钥解密后, 再md5加密与数据库密码比对

image.png

私钥的读取

WebMvcConfig
这里有个坑, 特别注意读取的string不能有—xx—或换行符, 常见的FileUtils都会拼接换行符
还有就是这个Bean没有配置在WebSecurityConfig中, 是由于bean加载的顺序问题

  1. @Configuration
  2. public class WebMvcConfig implements WebMvcConfigurer {
  3. @Value("classpath:rsa_private_key.pem")
  4. private Resource privateKey;
  5. /**
  6. * 此处有坑, 注意读取的时候--XX--不能要, 且不能有换行符
  7. */
  8. @Bean
  9. public PrivateKey loginPrivateKey() throws IOException, InvalidKeySpecException, NoSuchAlgorithmException {
  10. try (InputStream inputStream = privateKey.getInputStream()) {
  11. final List<String> lines = IOUtils.readLines(inputStream, StandardCharsets.UTF_8.name());
  12. StringBuilder builder = new StringBuilder();
  13. for (String line : lines) {
  14. if (!line.startsWith("--")) {
  15. builder.append(line);
  16. }
  17. }
  18. return SecurityUtils.getPrivateKey(builder.toString());
  19. }
  20. }
  21. }

web访问控制

WebSecurityConfig

  1. 注册filer以及provider
  2. 允许跨域
  3. 禁止csrf
  4. 禁止SpringSecurity默认的session策略, 因为我们用的自定义的token, 不用它的 ```java package top.xinzhang0618.oa.security;

import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; import top.xinzhang0618.oa.WebConstants;

@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

  1. @Autowired
  2. private UsernamePasswordProvider usernamePasswordProvider;
  3. @Autowired
  4. private UserTokenProvider userTokenProvider;
  5. @Override
  6. protected void configure(HttpSecurity http) throws Exception {
  7. http.csrf().disable().cors().and()
  8. .authorizeRequests()
  9. .antMatchers(HttpMethod.OPTIONS).permitAll()
  10. .antMatchers(HttpMethod.POST, WebConstants.LOGIN_URL, WebConstants.LOGOUT_URL).permitAll()
  11. .anyRequest().authenticated()
  12. .and()
  13. .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
  14. .and()
  15. .addFilterBefore(new UsernamePasswordFilter(WebConstants.LOGIN_URL, authenticationManager()),
  16. UsernamePasswordAuthenticationFilter.class)
  17. .addFilterBefore(new UserTokenFilter(authenticationManager()),
  18. UsernamePasswordAuthenticationFilter.class);
  19. }
  20. @Bean
  21. public CorsFilter corsFilter() {
  22. UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
  23. CorsConfiguration corsConfiguration = new CorsConfiguration();
  24. corsConfiguration.addAllowedOrigin("*");
  25. corsConfiguration.addAllowedHeader("*");
  26. corsConfiguration.addAllowedMethod("*");
  27. corsConfiguration.setAllowCredentials(true);
  28. source.registerCorsConfiguration("/**", corsConfiguration);
  29. return new CorsFilter(source);
  30. }
  31. @Override
  32. public void configure(WebSecurity web) throws Exception {
  33. super.configure(web);
  34. }
  35. @Override
  36. public void configure(AuthenticationManagerBuilder auth) {
  37. auth.authenticationProvider(usernamePasswordProvider);
  38. auth.authenticationProvider(userTokenProvider);
  39. }

}

  1. <a name="BsVvq"></a>
  2. ### 用户名密码认证
  3. 这里我们使用的自定义的校验方案, 也可以使用SpringSecurity的默认实现, 要实现UserService接口等等巴拉巴拉<br />一套校验方案包括, 自定义token, 自定义filter, 自定义provider<br />userToken
  4. ```java
  5. package top.xinzhang0618.oa.security;
  6. import org.springframework.security.authentication.AbstractAuthenticationToken;
  7. public class UserToken extends AbstractAuthenticationToken {
  8. private String token;
  9. public UserToken(String token, boolean authenticated) {
  10. super(null);
  11. this.setAuthenticated(authenticated);
  12. this.token = token;
  13. }
  14. @Override
  15. public Object getCredentials() {
  16. return null;
  17. }
  18. @Override
  19. public Object getPrincipal() {
  20. return token;
  21. }
  22. public String getToken() {
  23. return token;
  24. }
  25. }

UsernamePasswordFilter

  1. package top.xinzhang0618.oa.security;
  2. import com.alibaba.fastjson.JSON;
  3. import org.springframework.security.authentication.AuthenticationManager;
  4. import org.springframework.security.authentication.BadCredentialsException;
  5. import org.springframework.security.core.Authentication;
  6. import org.springframework.security.core.AuthenticationException;
  7. import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
  8. import top.xinzhang0618.oa.WebConstants;
  9. import top.xinzhang0618.oa.config.response.RestResponse;
  10. import top.xinzhang0618.oa.domain.User;
  11. import top.xinzhang0618.oa.util.StringUtils;
  12. import javax.servlet.FilterChain;
  13. import javax.servlet.ServletException;
  14. import javax.servlet.http.HttpServletRequest;
  15. import javax.servlet.http.HttpServletResponse;
  16. import java.io.IOException;
  17. public class UsernamePasswordFilter extends AbstractAuthenticationProcessingFilter {
  18. private final AuthenticationManager authenticationManager;
  19. public UsernamePasswordFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) {
  20. super(defaultFilterProcessesUrl);
  21. this.authenticationManager = authenticationManager;
  22. }
  23. @Override
  24. public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
  25. throws AuthenticationException, IOException, ServletException {
  26. User user = JSON.parseObject(request.getInputStream(), User.class);
  27. if (user == null || StringUtils.isEmpty(user.getUserName()) || StringUtils.isEmpty(user.getPassword())) {
  28. throw new BadCredentialsException("用户名或密码为空!");
  29. }
  30. TenantToken authenticationToken = new TenantToken(user.getUserName(), user.getPassword(), user.getTenantId());
  31. return authenticationManager.authenticate(authenticationToken);
  32. }
  33. @Override
  34. protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
  35. Authentication authResult) throws IOException, ServletException {
  36. UserLoginInfo loginInfo = (UserLoginInfo) authResult.getDetails();
  37. response.setContentType(WebConstants.CONTENT_TYPE);
  38. response.getWriter().write(JSON.toJSONString(RestResponse.success(loginInfo)));
  39. }
  40. @Override
  41. protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
  42. AuthenticationException failed) throws IOException, ServletException {
  43. response.sendError(HttpServletResponse.SC_UNAUTHORIZED, failed.getMessage());
  44. }
  45. }

UsernamePasswordProvider

  1. package top.xinzhang0618.oa.security;
  2. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  3. import org.springframework.beans.factory.annotation.Autowired;
  4. import org.springframework.security.authentication.AuthenticationProvider;
  5. import org.springframework.security.authentication.BadCredentialsException;
  6. import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
  7. import org.springframework.security.core.Authentication;
  8. import org.springframework.security.core.AuthenticationException;
  9. import org.springframework.security.core.userdetails.UsernameNotFoundException;
  10. import org.springframework.stereotype.Component;
  11. import top.xinzhang0618.oa.BizContext;
  12. import top.xinzhang0618.oa.domain.User;
  13. import top.xinzhang0618.oa.service.UserService;
  14. import top.xinzhang0618.oa.util.SecurityUtils;
  15. import top.xinzhang0618.oa.util.StringUtils;
  16. import javax.crypto.BadPaddingException;
  17. import javax.crypto.IllegalBlockSizeException;
  18. import javax.crypto.NoSuchPaddingException;
  19. import java.io.UnsupportedEncodingException;
  20. import java.security.InvalidKeyException;
  21. import java.security.NoSuchAlgorithmException;
  22. import java.security.PrivateKey;
  23. @Component
  24. public class UsernamePasswordProvider implements AuthenticationProvider {
  25. @Autowired
  26. private TokenManager tokenManager;
  27. @Autowired
  28. private UserService userService;
  29. @Autowired
  30. private PrivateKey privateKey;
  31. @Override
  32. public Authentication authenticate(Authentication authentication) throws AuthenticationException {
  33. TenantToken tenantToken = (TenantToken) authentication;
  34. if (StringUtils.isEmpty(tenantToken.getUserName())) {
  35. throw new UsernameNotFoundException("用户名不能为空");
  36. }
  37. try {
  38. // mybatisPlus的租户处理器会拼接tenant_id条件
  39. BizContext.setTenantId(tenantToken.getTenantId());
  40. String password = SecurityUtils.decryptRSA(tenantToken.getPassword(), privateKey);
  41. User user = userService.get(new LambdaQueryWrapper<User>().eq(User::getUserName,
  42. tenantToken.getUserName()));
  43. if (user == null) {
  44. throw new UsernameNotFoundException("未找到用户:" + tenantToken.getUserName());
  45. }
  46. if (!SecurityUtils.md5Hex(password).equals(user.getPassword())) {
  47. throw new BadCredentialsException("用户名或密码错误");
  48. }
  49. if (!user.isEnable()) {
  50. throw new BadCredentialsException("用户已禁用");
  51. }
  52. // 设置上下文
  53. BizContext.setUserId(user.getUserId());
  54. BizContext.setUserName(user.getUserName());
  55. UserLoginInfo loginInfo = new UserLoginInfo(user);
  56. tokenManager.generate(loginInfo);
  57. UsernamePasswordAuthenticationToken result =
  58. new UsernamePasswordAuthenticationToken(authentication.getPrincipal(),
  59. authentication.getCredentials());
  60. result.setDetails(loginInfo);
  61. return result;
  62. } catch (NoSuchAlgorithmException | IllegalBlockSizeException | InvalidKeyException
  63. | UnsupportedEncodingException | BadPaddingException | NoSuchPaddingException e) {
  64. throw new BadCredentialsException("用户名和密码格式错误");
  65. }
  66. }
  67. @Override
  68. public boolean supports(Class<?> authentication) {
  69. return authentication.equals(TenantToken.class);
  70. }
  71. }

认证完成后返回给前端的实体
UserLoginInfo

  1. package top.xinzhang0618.oa.security;
  2. import top.xinzhang0618.oa.domain.User;
  3. public class UserLoginInfo {
  4. private String token;
  5. private Long userId;
  6. private String nickname;
  7. private String userName;
  8. private String headUrl;
  9. private Long tenantId;
  10. public UserLoginInfo() {
  11. }
  12. public UserLoginInfo(User user) {
  13. this.userId = user.getUserId();
  14. this.nickname = user.getNickname();
  15. this.userName = user.getUserName();
  16. this.headUrl = user.getHeadUrl();
  17. this.tenantId = user.getTenantId();
  18. }
  19. public String getToken() {
  20. return token;
  21. }
  22. public void setToken(String token) {
  23. this.token = token;
  24. }
  25. public Long getUserId() {
  26. return userId;
  27. }
  28. public void setUserId(Long userId) {
  29. this.userId = userId;
  30. }
  31. public String getNickname() {
  32. return nickname;
  33. }
  34. public void setNickname(String nickname) {
  35. this.nickname = nickname;
  36. }
  37. public String getHeadUrl() {
  38. return headUrl;
  39. }
  40. public void setHeadUrl(String headUrl) {
  41. this.headUrl = headUrl;
  42. }
  43. public Long getTenantId() {
  44. return tenantId;
  45. }
  46. public void setTenantId(Long tenantId) {
  47. this.tenantId = tenantId;
  48. }
  49. public String getUserName() {
  50. return userName;
  51. }
  52. public void setUserName(String userName) {
  53. this.userName = userName;
  54. }
  55. }

token管理器, 这里直接用uuid生成的token, 用jwt意义不大
TokenManager

  1. package top.xinzhang0618.oa.security;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.data.redis.core.RedisTemplate;
  4. import org.springframework.stereotype.Component;
  5. import top.xinzhang0618.oa.WebConstants;
  6. import java.util.UUID;
  7. import java.util.concurrent.TimeUnit;
  8. @Component
  9. public class TokenManager {
  10. @Autowired
  11. private RedisTemplate<String, Object> redisTemplate;
  12. public UserLoginInfo getUser(String token) {
  13. return (UserLoginInfo) redisTemplate.opsForValue().get(buildKey(token));
  14. }
  15. /**
  16. * 生成token
  17. *
  18. * @param loginInfo
  19. * @return
  20. */
  21. public void generate(UserLoginInfo loginInfo) {
  22. String token = UUID.randomUUID().toString();
  23. loginInfo.setToken(token);
  24. redisTemplate.opsForValue().set(buildKey(token), loginInfo, 30L, TimeUnit.MINUTES);
  25. }
  26. /**
  27. * 移除token
  28. *
  29. * @param token
  30. */
  31. public void remove(String token) {
  32. redisTemplate.delete(token);
  33. }
  34. /**
  35. * token刷新
  36. *
  37. * @param token
  38. */
  39. public void refresh(String token) {
  40. redisTemplate.expire(buildKey(token), 30, TimeUnit.MINUTES);
  41. }
  42. private String buildKey(String token) {
  43. return WebConstants.TOKEN_PREFIX + token;
  44. }
  45. }

token认证

一样的三件, token, filter, provider
TenantToken

  1. package top.xinzhang0618.oa.security;
  2. import org.springframework.security.authentication.AbstractAuthenticationToken;
  3. public class TenantToken extends AbstractAuthenticationToken {
  4. private String userName;
  5. private String password;
  6. private Long tenantId;
  7. public TenantToken(String userName, String password,
  8. Long tenantId) {
  9. super(null);
  10. this.userName = userName;
  11. this.password = password;
  12. this.tenantId = tenantId;
  13. }
  14. public String getUserName() {
  15. return userName;
  16. }
  17. public String getPassword() {
  18. return password;
  19. }
  20. public Long getTenantId() {
  21. return tenantId;
  22. }
  23. @Override
  24. public Object getCredentials() {
  25. return password;
  26. }
  27. @Override
  28. public Object getPrincipal() {
  29. return userName;
  30. }
  31. }

UserTokenFilter

  1. package top.xinzhang0618.oa.security;
  2. import org.springframework.security.authentication.AuthenticationManager;
  3. import org.springframework.security.authentication.BadCredentialsException;
  4. import org.springframework.security.core.Authentication;
  5. import org.springframework.security.core.AuthenticationException;
  6. import org.springframework.security.core.context.SecurityContextHolder;
  7. import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
  8. import top.xinzhang0618.oa.WebConstants;
  9. import top.xinzhang0618.oa.util.StringUtils;
  10. import javax.servlet.FilterChain;
  11. import javax.servlet.ServletException;
  12. import javax.servlet.http.HttpServletRequest;
  13. import javax.servlet.http.HttpServletResponse;
  14. import java.io.IOException;
  15. public class UserTokenFilter extends BasicAuthenticationFilter {
  16. public UserTokenFilter(
  17. AuthenticationManager authenticationManager) {
  18. super(authenticationManager);
  19. }
  20. @Override
  21. protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
  22. throws IOException, ServletException {
  23. String token = request.getHeader(WebConstants.TOKEN_HEADER);
  24. if (!StringUtils.isEmpty(token)) {
  25. UserToken userToken = new UserToken(token, false);
  26. Authentication authenticate = getAuthenticationManager().authenticate(userToken);
  27. if (authenticate.isAuthenticated()) {
  28. SecurityContextHolder.getContext().setAuthentication(authenticate);
  29. chain.doFilter(request, response);
  30. return;
  31. }
  32. }
  33. this.onUnsuccessfulAuthentication(request, response, new BadCredentialsException("非法请求"));
  34. }
  35. @Override
  36. protected void onUnsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
  37. AuthenticationException failed) throws IOException {
  38. response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
  39. }
  40. }

UserTokenProvider

  1. package top.xinzhang0618.oa.security;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.security.authentication.AuthenticationProvider;
  4. import org.springframework.security.core.Authentication;
  5. import org.springframework.security.core.AuthenticationException;
  6. import org.springframework.stereotype.Component;
  7. import top.xinzhang0618.oa.BizContext;
  8. @Component
  9. public class UserTokenProvider implements AuthenticationProvider {
  10. @Autowired
  11. private TokenManager tokenManager;
  12. @Override
  13. public Authentication authenticate(Authentication authentication) throws AuthenticationException {
  14. UserToken userToken = (UserToken) authentication;
  15. String token = userToken.getToken();
  16. UserLoginInfo user = tokenManager.getUser(token);
  17. if (user == null) {
  18. return authentication;
  19. }
  20. tokenManager.refresh(token);
  21. BizContext.setUserId(user.getUserId());
  22. BizContext.setUserName(user.getUserName());
  23. BizContext.setTenantId(user.getTenantId());
  24. userToken.setAuthenticated(true);
  25. return authentication;
  26. }
  27. @Override
  28. public boolean supports(Class<?> aClass) {
  29. return aClass.equals(UserToken.class);
  30. }
  31. }

常量类
WebConstants

  1. package top.xinzhang0618.oa;
  2. /**
  3. * @author buer
  4. * @version 2018-01-17 14:53
  5. */
  6. public final class WebConstants {
  7. public static final String TOKEN_HEADER = "Authorization";
  8. public static final String TOKEN_PREFIX = "LOGIN_TOKEN:";
  9. public static final String CORS_REQUEST_METHOD = "OPTIONS";
  10. public static final String LOGIN_URL = "/login";
  11. public static final String LOGOUT_URL = "/logout";
  12. public static final String CONTENT_TYPE = "application/json;charset=utf-8";
  13. public static final String HEAD_CORS_ALLOW_ORIGIN = "Access-Control-Allow-Origin";
  14. public static final String HEAD_CORS_ALLOW_METHODS = "Access-Control-Allow-Methods";
  15. public static final String HEAD_CORS_ALLOW_HEADERS = "Access-Control-Allow-Headers";
  16. public static final String HEAD_CORS_MAX_AGE = "Access-Control-Max-Age";
  17. public static final String HEAD_CORS_ALLOW_ORIGIN_VALUE = "*";
  18. public static final String HEAD_CORS_ALLOW_METHODS_VALUE = "*";
  19. public static final String HEAD_CORS_ALLOW_HEADERS_VALUE = "Origin, X-Requested-With, Content-Type, Accept, " +
  20. "Authorization, X-OA-ORGAN";
  21. public static final String HEAD_CORS_MAX_AGE_VALUE = "3600";
  22. }