Skip to content

Commit

Permalink
feat: 支持小程序登录
Browse files Browse the repository at this point in the history
  • Loading branch information
lichong-a committed Aug 28, 2024
1 parent 8175862 commit 7130850
Show file tree
Hide file tree
Showing 10 changed files with 393 additions and 12 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ buildscript {
set('springCloudAlibabaVersion', '2023.0.1.2')
set('redissonVersion', '3.31.0')
set('cosVersion', '5.6.225')
set('gsonVersion', '2.11.0')
set('knife4jVersion', '4.4.0')
set('springDocVersion', '2.5.0')
set('jjwtVersion', '0.12.5')
Expand Down
1 change: 1 addition & 0 deletions common/core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ dependencies {
api "com.github.penggle:kaptcha:${kaptchaVersion}"
api "com.github.xiaoymin:knife4j-openapi3-jakarta-spring-boot-starter:${knife4jVersion}"
api "com.qcloud:cos_api:${cosVersion}"
api "com.google.code.gson:gson:${gsonVersion}"
api "io.jsonwebtoken:jjwt-api:${jjwtVersion}"
api "io.jsonwebtoken:jjwt-impl:${jjwtVersion}"
api "io.jsonwebtoken:jjwt-jackson:${jjwtVersion}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@ public class ApplicationConfig {
@NestedConfigurationProperty
private final Security security;

public static final String WECHAT_LOGIN_URL_TEMPLATE = "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code";

/**
* @param adminUsername 管理员用户名
* @param adminPassword 管理员密码
* @param token token相关配置
* @param weChat 微信小程序
* @param logoutSuccessUrl 注销成功跳转地址
* @param loginPage 登录页地址,默认:/login
* @param corsAllowedOrigins 允许跨域的域名
Expand All @@ -35,6 +38,7 @@ public class ApplicationConfig {
public record Security(String adminUsername,
String adminPassword,
@NestedConfigurationProperty Token token,
@NestedConfigurationProperty WeChat weChat,
String logoutSuccessUrl,
String loginPage,
List<String> corsAllowedOrigins,
Expand All @@ -48,6 +52,25 @@ public record Token(String signingKey,
Long expiration,
Long refreshExpiration) {
}

/**
* @param appId 小程序ID
* @param appSecret 小程序密钥
*/
public record WeChat(String appId,
String appSecret) {
}
}

/**
* 生成微信登录请求地址
*
* @param code code
* @return 请求地址
*/
public String wechatLoginUrl(String code) {
return String.format(WECHAT_LOGIN_URL_TEMPLATE, security.weChat.appId, security.weChat.appSecret, code);
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@
import org.apache.commons.lang3.StringUtils;
import org.funcode.portal.server.common.core.config.ApplicationConfig;
import org.funcode.portal.server.common.core.security.filter.JwtTokenFilter;
import org.funcode.portal.server.common.core.security.filter.WechatAuthenticationFilter;
import org.funcode.portal.server.common.core.security.handler.CustomAuthenticationFailureHandler;
import org.funcode.portal.server.common.core.security.handler.CustomAuthenticationSuccessHandler;
import org.funcode.portal.server.common.core.security.handler.WechatAuthenticationFailureHandler;
import org.funcode.portal.server.common.core.security.handler.WechatAuthenticationSuccessHandler;
import org.funcode.portal.server.common.core.security.provider.WeChatAuthenticationProvider;
import org.funcode.portal.server.common.core.security.service.impl.UserDetailsServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
Expand All @@ -34,6 +38,7 @@
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import static org.funcode.portal.server.common.core.constant.SecurityConstant.TOKEN_HEADER_KEY;
import static org.funcode.portal.server.common.core.security.filter.WechatAuthenticationFilter.WECHAT_LOGIN_PATH;
import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS;
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher;

Expand All @@ -56,12 +61,15 @@ public class DefaultSecurityConfig {
private final CustomAuthenticationFailureHandler customAuthenticationFailureHandler;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
public SecurityFilterChain securityFilterChain(HttpSecurity http,
WeChatAuthenticationProvider weChatAuthenticationProvider,
WechatAuthenticationFilter weChatAuthenticationFilter) throws Exception {
String defaultLoginPage = StringUtils.isBlank(applicationConfig.getSecurity().loginPage()) ? "/login" : applicationConfig.getSecurity().loginPage();
// HTTP 的 Clear-Site-Data 标头是浏览器支持的指令,用于清除属于拥有网站的 Cookie、存储和缓存
HeaderWriterLogoutHandler clearSiteData = new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(ClearSiteDataHeaderWriter.Directive.ALL));
http.cors(cors -> cors
.configurationSource(corsWebsiteConfigurationSource()))
http
.cors(cors -> cors
.configurationSource(corsWebsiteConfigurationSource()))
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(request -> request
.requestMatchers(
Expand All @@ -75,10 +83,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
"/doc.html",
defaultLoginPage
).permitAll()
.requestMatchers(antMatcher("/**/anonymous"))
.permitAll()
.anyRequest()
.authenticated())
.requestMatchers(HttpMethod.POST, WECHAT_LOGIN_PATH).permitAll()
.requestMatchers(antMatcher("/**/anonymous")).permitAll()
.anyRequest().authenticated())
.sessionManagement(manager -> manager.sessionCreationPolicy(STATELESS))
.formLogin(login -> login
.usernameParameter("username")
Expand All @@ -93,20 +100,32 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.logoutSuccessUrl(
StringUtils.isBlank(applicationConfig.getSecurity().logoutSuccessUrl()) ? "/login?logout" : applicationConfig.getSecurity().logoutSuccessUrl())
)
.authenticationProvider(authenticationProvider())
.authenticationProvider(weChatAuthenticationProvider)
.authenticationProvider(daoAuthenticationProvider())
.addFilterBefore(
jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
jwtTokenFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(
weChatAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}

@Bean
public AuthenticationProvider authenticationProvider() {
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}

@Bean
public WechatAuthenticationFilter weChatAuthenticationFilter(AuthenticationManager authenticationManager,
WechatAuthenticationSuccessHandler wechatAuthenticationSuccessHandler,
WechatAuthenticationFailureHandler wechatAuthenticationFailureHandler) {
WechatAuthenticationFilter filter = new WechatAuthenticationFilter(authenticationManager, wechatAuthenticationSuccessHandler, wechatAuthenticationFailureHandler);
filter.setAuthenticationManager(authenticationManager);
return filter;
}

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
Expand All @@ -117,7 +136,7 @@ public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

CorsConfigurationSource corsWebsiteConfigurationSource() {
private CorsConfigurationSource corsWebsiteConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(applicationConfig.getSecurity().corsAllowedOrigins());
configuration.setAllowedMethods(applicationConfig.getSecurity().corsAllowedMethods());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright 2024 李冲. All rights reserved.
*
*/

package org.funcode.portal.server.common.core.security.domain;

import lombok.Getter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

/**
* 微信登录认证Token
*
* @author 李冲
* @see <a href="https://lichong.work">李冲博客</a>
* @since 0.0.1
*/
@Getter
public class WechatAuthenticationToken extends AbstractAuthenticationToken {
private final String openId;
private final Object principal;

/**
* 未认证的Token
*
* @param openId openId
*/
public WechatAuthenticationToken(String openId) {
super(null);
this.openId = openId;
this.principal = openId;
setAuthenticated(false);
}

/**
* 已认证的token
*
* @param openId openId
* @param authorities 权限
*/
public WechatAuthenticationToken(String openId, Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.openId = openId;
this.principal = principal;
setAuthenticated(true);
}

/**
* 未认证的Token
*
* @param openId openId
*/
public static WechatAuthenticationToken unauthenticated(String openId) {
return new WechatAuthenticationToken(openId);
}

/**
* 已认证的token
*
* @param openId openId
* @param authorities 权限
*/
public static WechatAuthenticationToken authenticated(String openId, Object principal, Collection<? extends GrantedAuthority> authorities) {
return new WechatAuthenticationToken(openId, principal, authorities);
}

@Override
public Object getCredentials() {
return null;
}

@Override
public Object getPrincipal() {
return this.principal;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 2024 李冲. All rights reserved.
*
*/

package org.funcode.portal.server.common.core.security.filter;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.funcode.portal.server.common.core.security.domain.WechatAuthenticationToken;
import org.funcode.portal.server.common.core.security.handler.WechatAuthenticationFailureHandler;
import org.funcode.portal.server.common.core.security.handler.WechatAuthenticationSuccessHandler;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.ObjectUtils;

/**
* 微信登录认证
*
* @author 李冲
* @see <a href="https://lichong.work">李冲博客</a>
* @since 0.0.1
*/
public class WechatAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
/**
* 微信登录路径
*/
public static final String WECHAT_LOGIN_PATH = "/wechat/login";
/**
* 允许的请求方法
*/
public static final String METHOD = "POST";
/**
* 参数名称
*/
public static final String PARAM_KEY = "code";
/**
* 路径匹配
*/
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(WECHAT_LOGIN_PATH, METHOD);


public WechatAuthenticationFilter(AuthenticationManager authenticationManager,
WechatAuthenticationSuccessHandler wechatAuthenticationSuccessHandler,
WechatAuthenticationFailureHandler wechatAuthenticationFailureHandler) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
setAuthenticationFailureHandler(wechatAuthenticationFailureHandler);
setAuthenticationSuccessHandler(wechatAuthenticationSuccessHandler);
}

/**
* 从请求中取出code参数,向微信服务器发送请求,如果成功获取到openID,则创建一个未认证的 WechatAuthenticationToken , 并将其提交给 AuthenticationManager
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!METHOD.equals(request.getMethod())) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
final String code = request.getParameter(PARAM_KEY);
if (ObjectUtils.isEmpty(code)) {
throw new AuthenticationServiceException("code 不允许为空");
}
// 创建一个未认证的token,放入code
final WechatAuthenticationToken token = WechatAuthenticationToken.unauthenticated(code);
// 设置details
token.setDetails(this.authenticationDetailsSource.buildDetails(request));
// 将token提交给 AuthenticationManager
return this.getAuthenticationManager().authenticate(token);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2024 李冲. All rights reserved.
*
*/

package org.funcode.portal.server.common.core.security.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.funcode.portal.server.common.core.base.http.response.ResponseResult;
import org.funcode.portal.server.common.core.base.http.response.ResponseStatusEnum;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;

/**
* @author 李冲
* @see <a href="https://lichong.work">李冲博客</a>
* @since 0.0.1
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class WechatAuthenticationFailureHandler implements AuthenticationFailureHandler {

@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
ObjectMapper mapper = new ObjectMapper();
response.getWriter().write(mapper.writeValueAsString(ResponseResult.fail(exception.getLocalizedMessage(), ResponseStatusEnum.HTTP_STATUS_401)));
}
}
Loading

0 comments on commit 7130850

Please sign in to comment.