ソースを参照

[Feature] Message Send Interface: Add HMAC authentication

AA 11 ヶ月 前
コミット
0ce9a7b951

+ 2 - 0
db/db240716-addAppKeyAndSecret.sql

@@ -0,0 +1,2 @@
+ALTER TABLE portal.app_info ADD app_key varchar(32) NULL;
+ALTER TABLE portal.app_info ADD app_secret varchar(32) NULL;

+ 71 - 15
src/main/java/com/dragon/tj/portal/auth/config/WebSecurityConfig.java

@@ -1,13 +1,21 @@
 package com.dragon.tj.portal.auth.config;
 
-import com.dragon.tj.portal.auth.service.*;
+import com.dragon.tj.portal.auth.module.hmac.HmacAuthenticationFilter;
+import com.dragon.tj.portal.auth.service.JwtTokenAuthenticationFilter;
+import com.dragon.tj.portal.auth.service.JwtTokenLogoutSuccessHandler;
+import com.dragon.tj.portal.auth.service.MyCasAuthenticationEntryPoint;
+import com.dragon.tj.portal.auth.service.MySimpleUrlAuthenticationSuccessHandler;
+import com.dragon.tj.portal.auth.service.MyUserDetailsByNameServiceWrapper;
+import com.dragon.tj.portal.auth.service.MyUserDetailsService;
 import org.jasig.cas.client.validation.Cas20ServiceTicketValidator;
 import org.jasig.cas.client.validation.TicketValidator;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
-import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.security.authentication.ProviderManager;
 import org.springframework.security.cas.ServiceProperties;
 import org.springframework.security.cas.authentication.CasAuthenticationProvider;
@@ -15,11 +23,17 @@ import org.springframework.security.cas.web.CasAuthenticationFilter;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
 import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
+import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
 import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.web.AuthenticationEntryPoint;
 import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.AuthenticationEntryPointFailureHandler;
 import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+
+import java.nio.charset.StandardCharsets;
 
 @Configuration
 @EnableWebSecurity
@@ -46,7 +60,7 @@ public class WebSecurityConfig {
     private String casFailureUrl;
 
     @Autowired
-    private ServletWebServerApplicationContext context;
+    private JdbcTemplate jdbcTemplate;
 
     @Autowired
     private JwtTokenAuthenticationFilter jwtTokenAuthenticationFilter;
@@ -57,39 +71,81 @@ public class WebSecurityConfig {
     @Autowired
     private JwtTokenLogoutSuccessHandler logoutSuccessHandler;
 
-    private static StringBuilder whiteList = new StringBuilder();
-    private static String whiteListSplit = ",";
+    private static final StringBuilder whiteList = new StringBuilder();
+    private static final String DELIMITER_COMMA = ",";
 
-    /**
-     *
-     *白名单
-     */
     static {
-        whiteList.append("/test/login" + whiteListSplit);
-        whiteList.append("/file/**" + whiteListSplit);
+        // 白名单
+        whiteList.append("/test/login").append(DELIMITER_COMMA)
+                .append("/file/**").append(DELIMITER_COMMA);
     }
 
     @Bean
     public WebSecurityCustomizer webSecurityCustomizer() {
-
         return (web) -> web.ignoring()
                 // 认证成功后才会忽略
                 .antMatchers("/resources/**");
     }
 
+    @Bean
+    public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
+        http
+                .antMatcher("/api/**")
+                // CSRF禁用,因为不使用session
+                .csrf(CsrfConfigurer::disable)
+                .authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
+                .sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+                .exceptionHandling(exceptions -> exceptions.authenticationEntryPoint(globalAuthenticationEntryPoint()))
+                // 内置filters已经排序 FilterOrderRegistration.FilterOrderRegistration()
+                .addFilterBefore(hmacAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
+        return http.build();
+    }
+
+    // TODO: 2024/7/17 sai Delete
+    public AuthenticationEntryPoint globalAuthenticationEntryPoint() {
+        return (request, response, authException) -> {
+            response.setStatus(HttpStatus.UNAUTHORIZED.value());
+            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+            response.setContentType(MediaType.APPLICATION_JSON.toString());
+            response.getWriter().write("{\"message:\":\"Global认证失败:" + authException.getMessage() + "\"}");
+        };
+    }
+
+    public AuthenticationEntryPoint apiAuthenticationEntryPoint() {
+        return (request, response, authException) -> {
+            response.setStatus(HttpStatus.UNAUTHORIZED.value());
+            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+            response.setContentType(MediaType.APPLICATION_JSON.toString());
+            response.getWriter().write("{\"message:\":\"认证失败:" + authException.getMessage() + "\"}");
+        };
+    }
+
+    public HmacAuthenticationFilter hmacAuthenticationFilter() {
+        // HmacAuthenticationFilter filter = new HmacAuthenticationFilter("/api/token");
+        HmacAuthenticationFilter filter = new HmacAuthenticationFilter("/api/**");
+        filter.setJdbcTemplate(jdbcTemplate);
+        filter.setAuthenticationFailureHandler(new AuthenticationEntryPointFailureHandler(apiAuthenticationEntryPoint()));
+        filter.setAuthenticationSuccessHandler((request, response, authentication) -> {
+            SecurityContextHolder.getContext().setAuthentication(authentication);
+        });
+        return filter;
+    }
 
     @Bean
     public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
         http
                 // CSRF禁用,因为不使用session
-                .csrf().disable().cors()
+                .csrf().disable()
+                // Enable CORS
+                .cors()
                 .and()
                 .authorizeRequests()
-                .antMatchers(whiteList.toString().split(whiteListSplit)).permitAll()
+                .antMatchers(whiteList.toString().split(DELIMITER_COMMA)).permitAll()
                 .anyRequest().authenticated()
                 .and()
                 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                 .and()
+                // 因为CasAuthenticationFilter仅拦截/sso/login,所以未认证前访问其他url失败时都走到这个兜底的exception处理
                 .exceptionHandling(exceptions -> exceptions.authenticationEntryPoint(authenticationEntryPoint()))
                 .addFilter(casAuthenticationFilter())
                 .addFilterBefore(jwtTokenAuthenticationFilter, CasAuthenticationFilter.class)
@@ -110,13 +166,13 @@ public class WebSecurityConfig {
     public CasAuthenticationFilter casAuthenticationFilter() {
         CasAuthenticationFilter filter = new CasAuthenticationFilter();
         filter.setFilterProcessesUrl(casFilterUrl);
-        filter.setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler(casFailureUrl));
 
         CasAuthenticationProvider casAuthenticationProvider = casAuthenticationProvider(userDetailsService);
         filter.setAuthenticationManager(new ProviderManager(casAuthenticationProvider));
 
         mySimpleUrlAuthenticationSuccessHandler.setDefaultTargetUrl(casTargetUrl);
         filter.setAuthenticationSuccessHandler(mySimpleUrlAuthenticationSuccessHandler);
+        filter.setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler(casFailureUrl));
 
         return filter;
     }

+ 29 - 0
src/main/java/com/dragon/tj/portal/auth/config/WebServletConfig.java

@@ -0,0 +1,29 @@
+package com.dragon.tj.portal.auth.config;
+
+import com.dragon.tj.portal.auth.module.hmac.HmacAuthenticationFilter;
+import com.dragon.tj.portal.auth.service.JwtTokenAuthenticationFilter;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class WebServletConfig {
+
+    /**
+     * 禁用ServletFilterChain中的JwtTokenAuthenticationFilter
+     * 该filter只用于SecurityFilterChain中,禁用以避免执行两次
+     */
+    @Bean
+    public FilterRegistrationBean<JwtTokenAuthenticationFilter> jwtTokenFilterRegistration(JwtTokenAuthenticationFilter filter) {
+        FilterRegistrationBean<JwtTokenAuthenticationFilter> registration = new FilterRegistrationBean<>(filter);
+        registration.setEnabled(false);
+        return registration;
+    }
+
+    // @Bean
+    public FilterRegistrationBean<HmacAuthenticationFilter> hmacFilterRegistration(HmacAuthenticationFilter filter) {
+        FilterRegistrationBean<HmacAuthenticationFilter> registration = new FilterRegistrationBean<>(filter);
+        registration.setEnabled(false);
+        return registration;
+    }
+}

+ 98 - 0
src/main/java/com/dragon/tj/portal/auth/module/hmac/HmacAuthenticationFilter.java

@@ -0,0 +1,98 @@
+package com.dragon.tj.portal.auth.module.hmac;
+
+import org.apache.commons.codec.digest.HmacAlgorithms;
+import org.apache.commons.codec.digest.HmacUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+public class HmacAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
+    private static final Logger LOGGER = LoggerFactory.getLogger(HmacAuthenticationFilter.class);
+
+    private JdbcTemplate jdbcTemplate;
+
+    public HmacAuthenticationFilter(String defaultFilterProcessesUrl) {
+        super(defaultFilterProcessesUrl);
+    }
+
+    @Override
+    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
+        String appKey = request.getHeader("appKey");
+        if (StringUtils.isBlank(appKey)) {
+            throw new BadCredentialsException("未设置appKey");
+        }
+        // 时间戳验证
+        String timestamp = request.getHeader("timestamp");
+        if (StringUtils.isBlank(timestamp)) {
+            throw new BadCredentialsException("未设置timestamp");
+        }
+        // 大于5分钟,非法请求
+        long diff = System.currentTimeMillis() - Long.parseLong(timestamp);
+        // TODO: 2024/7/17 sai default timeout 5m
+        if (Math.abs(diff) > 1000 * 60 * 500) {
+            throw new BadCredentialsException("请求超时");
+        }
+        // 随机字符串,防止重复提交
+        String nonce = request.getHeader("nonce");
+        if (StringUtils.isEmpty(nonce)) {
+            throw new BadCredentialsException("未设置nonce");
+        }
+        // 验证签名
+        String signature = request.getHeader("signature");
+        if (StringUtils.isEmpty(nonce)) {
+            throw new BadCredentialsException("未设置signature");
+        }
+
+        String appSecret;
+        try {
+            appSecret = jdbcTemplate.queryForObject("select app_secret from app_info where app_key = ?",
+                    String.class, appKey);
+        } catch (Exception e) {
+            throw new BadCredentialsException("查询应用出错,appKey: " + appKey, e);
+        }
+        if (StringUtils.isEmpty(appSecret)) {
+            throw new BadCredentialsException("未查询到该应用,appKey: " + appKey);
+        }
+
+        String signPayload = String.format("%s:%s:%s", appKey, timestamp, nonce);
+        String signResult = new HmacUtils(HmacAlgorithms.HMAC_SHA_256, appSecret).hmacHex(signPayload);
+        if (!signature.equals(signResult)) {
+            throw new BadCredentialsException("Signature验证错误");
+        }
+        // TODO: 2024/7/17 sai
+
+        // authorities参数必填使authenticated为true
+        return new UsernamePasswordAuthenticationToken(appKey, signature, null);
+    }
+
+    @Override
+    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
+        super.successfulAuthentication(request, response, chain, authResult);
+        // continue next filter
+        chain.doFilter(request, response);
+    }
+
+    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
+        this.jdbcTemplate = jdbcTemplate;
+    }
+
+    public static void main(String[] args) {
+        long l = System.currentTimeMillis();
+        System.out.println("timestamp: " + l);
+        String appSecret = "c5f9de99eb3641a98ac49b9d743dab4d";
+        String signPayload = String.format("%s:%s:%s", "cfaf75fe4cc945f789bc6518354ded53", l, "123455");
+        System.out.println(new HmacUtils(HmacAlgorithms.HMAC_SHA_256, appSecret).hmacHex(signPayload));
+    }
+}

+ 4 - 3
src/main/java/com/dragon/tj/portal/auth/service/JwtTokenAuthenticationFilter.java

@@ -2,7 +2,6 @@ package com.dragon.tj.portal.auth.service;
 
 import com.dragon.tj.portal.auth.model.LoginUser;
 import com.dragon.tj.portal.auth.util.SecurityUtils;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
@@ -19,9 +18,11 @@ import java.util.Objects;
 @Component
 public class JwtTokenAuthenticationFilter extends OncePerRequestFilter {
 
-    @Autowired
-    private TokenService tokenService;
+    private final TokenService tokenService;
 
+    public JwtTokenAuthenticationFilter(TokenService tokenService) {
+        this.tokenService = tokenService;
+    }
 
     @Override
     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {