Преглед изворни кода

:heavy_plus_sign: 增加验证码校验、增加CaptchaTokenGranter

smallchill пре 6 година
родитељ
комит
cdcc45037d

+ 9 - 0
blade-auth/pom.xml

@@ -36,6 +36,10 @@
             <groupId>org.springblade</groupId>
             <artifactId>blade-core-cloud</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.springblade</groupId>
+            <artifactId>blade-starter-redis</artifactId>
+        </dependency>
         <dependency>
             <groupId>org.springblade</groupId>
             <artifactId>blade-user-api</artifactId>
@@ -73,6 +77,11 @@
             <artifactId>jjwt</artifactId>
             <version>0.9.1</version>
         </dependency>
+        <!-- 验证码 -->
+        <dependency>
+            <groupId>com.github.whvcse</groupId>
+            <artifactId>easy-captcha</artifactId>
+        </dependency>
         <!-- 链路追踪、服务监控 -->
         <!--<dependency>
             <groupId>org.springblade</groupId>

+ 7 - 20
blade-auth/src/main/java/org/springblade/auth/config/BladeAuthorizationServerConfiguration.java

@@ -19,7 +19,10 @@ package org.springblade.auth.config;
 import lombok.AllArgsConstructor;
 import lombok.SneakyThrows;
 import org.springblade.auth.constant.AuthConstant;
+import org.springblade.auth.granter.BladeTokenGranter;
+import org.springblade.auth.props.AuthProperties;
 import org.springblade.auth.service.BladeClientDetailsServiceImpl;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.core.annotation.Order;
 import org.springframework.security.authentication.AuthenticationManager;
@@ -29,14 +32,9 @@ import org.springframework.security.oauth2.config.annotation.web.configuration.A
 import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
 import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
 import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
-import org.springframework.security.oauth2.provider.token.TokenEnhancer;
-import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
 import org.springframework.security.oauth2.provider.token.TokenStore;
-import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
 
 import javax.sql.DataSource;
-import java.util.ArrayList;
-import java.util.List;
 
 /**
  * 认证服务器配置
@@ -47,6 +45,7 @@ import java.util.List;
 @Configuration
 @AllArgsConstructor
 @EnableAuthorizationServer
+@EnableConfigurationProperties(AuthProperties.class)
 public class BladeAuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
 
 	private final DataSource dataSource;
@@ -57,26 +56,14 @@ public class BladeAuthorizationServerConfiguration extends AuthorizationServerCo
 
 	private TokenStore tokenStore;
 
-	private JwtAccessTokenConverter jwtAccessTokenConverter;
-
-	private TokenEnhancer jwtTokenEnhancer;
+	private BladeTokenGranter tokenGranter;
 
 	@Override
 	public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
 		endpoints.tokenStore(tokenStore)
 			.authenticationManager(authenticationManager)
-			.userDetailsService(userDetailsService);
-		//扩展token返回结果
-		if (jwtAccessTokenConverter != null && jwtTokenEnhancer != null) {
-			TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
-			List<TokenEnhancer> enhancerList = new ArrayList<>();
-			enhancerList.add(jwtTokenEnhancer);
-			enhancerList.add(jwtAccessTokenConverter);
-			tokenEnhancerChain.setTokenEnhancers(enhancerList);
-			//jwt
-			endpoints.tokenEnhancer(tokenEnhancerChain)
-				.accessTokenConverter(jwtAccessTokenConverter);
-		}
+			.userDetailsService(userDetailsService)
+			.tokenGranter(tokenGranter);
 	}
 
 	/**

+ 7 - 1
blade-auth/src/main/java/org/springblade/auth/config/BladeResourceServerConfiguration.java

@@ -47,7 +47,13 @@ public class BladeResourceServerConfiguration extends ResourceServerConfigurerAd
 			.successHandler(appLoginInSuccessHandler)
 			.and()
 			.authorizeRequests()
-			.antMatchers("/actuator/**", "/token/**", "/mobile/**", "/v2/api-docs", "/v2/api-docs-ext").permitAll()
+			.antMatchers(
+				"/actuator/**",
+				"/token/**",
+				"/oauth/captcha",
+				"/mobile/**",
+				"/v2/api-docs",
+				"/v2/api-docs-ext").permitAll()
 			.anyRequest().authenticated().and()
 			.csrf().disable();
 	}

+ 66 - 0
blade-auth/src/main/java/org/springblade/auth/config/BladeTokenGranterConfiguration.java

@@ -0,0 +1,66 @@
+/*
+ *      Copyright (c) 2018-2028, Chill Zhuang All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are met:
+ *
+ *  Redistributions of source code must retain the above copyright notice,
+ *  this list of conditions and the following disclaimer.
+ *  Redistributions in binary form must reproduce the above copyright
+ *  notice, this list of conditions and the following disclaimer in the
+ *  documentation and/or other materials provided with the distribution.
+ *  Neither the name of the dreamlu.net developer nor the names of its
+ *  contributors may be used to endorse or promote products derived from
+ *  this software without specific prior written permission.
+ *  Author: Chill 庄骞 (smallchill@163.com)
+ */
+package org.springblade.auth.config;
+
+import lombok.AllArgsConstructor;
+import org.springblade.auth.granter.BladeTokenGranter;
+import org.springblade.auth.props.AuthProperties;
+import org.springblade.core.redis.cache.BladeRedisCache;
+import org.springblade.system.user.feign.IUserClient;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.oauth2.provider.token.TokenEnhancer;
+import org.springframework.security.oauth2.provider.token.TokenStore;
+import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
+
+import javax.sql.DataSource;
+
+/**
+ * 自定义TokenGranter配置类
+ *
+ * @author Chill
+ */
+@Configuration
+@AllArgsConstructor
+public class BladeTokenGranterConfiguration {
+
+	private final DataSource dataSource;
+
+	private AuthenticationManager authenticationManager;
+
+	private UserDetailsService userDetailsService;
+
+	private TokenStore tokenStore;
+
+	private TokenEnhancer jwtTokenEnhancer;
+
+	private JwtAccessTokenConverter jwtAccessTokenConverter;
+
+	private AuthProperties authProperties;
+
+	private IUserClient userClient;
+
+	private BladeRedisCache redisCache;
+
+	@Bean
+	public BladeTokenGranter bladeTokenGranter() {
+		return new BladeTokenGranter(dataSource, authenticationManager, userDetailsService, tokenStore, jwtTokenEnhancer, jwtAccessTokenConverter, authProperties, userClient, redisCache);
+	}
+
+}

+ 20 - 0
blade-auth/src/main/java/org/springblade/auth/endpoint/BladeTokenEndPoint.java

@@ -16,13 +16,20 @@
  */
 package org.springblade.auth.endpoint;
 
+import com.wf.captcha.SpecCaptcha;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.springblade.common.cache.CacheNames;
+import org.springblade.core.redis.cache.BladeRedisCache;
 import org.springblade.core.tool.api.R;
+import org.springblade.core.tool.support.Kv;
 import org.springframework.security.core.Authentication;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RestController;
 
+import java.time.Duration;
+import java.util.UUID;
+
 /**
  * BladeEndPoint
  *
@@ -33,9 +40,22 @@ import org.springframework.web.bind.annotation.RestController;
 @AllArgsConstructor
 public class BladeTokenEndPoint {
 
+	private BladeRedisCache redisCache;
+
 	@GetMapping("/oauth/user-info")
 	public R<Authentication> currentUser(Authentication authentication) {
 		return R.data(authentication);
 	}
 
+	@GetMapping("/oauth/captcha")
+	public Kv captcha() {
+		SpecCaptcha specCaptcha = new SpecCaptcha(130, 48, 5);
+		String verCode = specCaptcha.text().toLowerCase();
+		String key = UUID.randomUUID().toString();
+		// 存入redis并设置过期时间为30分钟
+		redisCache.setEx(CacheNames.CAPTCHA_KEY + key, verCode, Duration.ofMinutes(30));
+		// 将key和base64返回给前端
+		return Kv.create().set("key", key).set("image", specCaptcha.toBase64());
+	}
+
 }

+ 157 - 0
blade-auth/src/main/java/org/springblade/auth/granter/BladeTokenGranter.java

@@ -0,0 +1,157 @@
+/*
+ *      Copyright (c) 2018-2028, Chill Zhuang All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are met:
+ *
+ *  Redistributions of source code must retain the above copyright notice,
+ *  this list of conditions and the following disclaimer.
+ *  Redistributions in binary form must reproduce the above copyright
+ *  notice, this list of conditions and the following disclaimer in the
+ *  documentation and/or other materials provided with the distribution.
+ *  Neither the name of the dreamlu.net developer nor the names of its
+ *  contributors may be used to endorse or promote products derived from
+ *  this software without specific prior written permission.
+ *  Author: Chill 庄骞 (smallchill@163.com)
+ */
+package org.springblade.auth.granter;
+
+import org.springblade.auth.constant.AuthConstant;
+import org.springblade.auth.props.AuthProperties;
+import org.springblade.auth.service.BladeClientDetailsServiceImpl;
+import org.springblade.core.redis.cache.BladeRedisCache;
+import org.springblade.system.user.feign.IUserClient;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.ProviderManager;
+import org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.oauth2.common.OAuth2AccessToken;
+import org.springframework.security.oauth2.provider.*;
+import org.springframework.security.oauth2.provider.client.ClientCredentialsTokenGranter;
+import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
+import org.springframework.security.oauth2.provider.code.AuthorizationCodeTokenGranter;
+import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices;
+import org.springframework.security.oauth2.provider.implicit.ImplicitTokenGranter;
+import org.springframework.security.oauth2.provider.password.ResourceOwnerPasswordTokenGranter;
+import org.springframework.security.oauth2.provider.refresh.RefreshTokenGranter;
+import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory;
+import org.springframework.security.oauth2.provider.token.*;
+import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
+import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider;
+
+import javax.sql.DataSource;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 自定义拓展TokenGranter
+ *
+ * @author Chill
+ */
+public class BladeTokenGranter implements TokenGranter {
+
+	private final DataSource dataSource;
+
+	private AuthenticationManager authenticationManager;
+
+	private UserDetailsService userDetailsService;
+
+	private TokenStore tokenStore;
+
+	private TokenEnhancer jwtTokenEnhancer;
+
+	private JwtAccessTokenConverter jwtAccessTokenConverter;
+
+	private CompositeTokenGranter delegate;
+
+	private AuthProperties authProperties;
+
+	private IUserClient userClient;
+
+	private BladeRedisCache redisCache;
+
+	public BladeTokenGranter(DataSource dataSource, AuthenticationManager authenticationManager, UserDetailsService userDetailsService, TokenStore tokenStore, TokenEnhancer jwtTokenEnhancer, JwtAccessTokenConverter jwtAccessTokenConverter, AuthProperties authProperties, IUserClient userClient, BladeRedisCache redisCache) {
+		this.dataSource = dataSource;
+		this.authenticationManager = authenticationManager;
+		this.userDetailsService = userDetailsService;
+		this.tokenStore = tokenStore;
+		this.jwtTokenEnhancer = jwtTokenEnhancer;
+		this.jwtAccessTokenConverter = jwtAccessTokenConverter;
+		this.userClient = userClient;
+		this.authProperties = authProperties;
+		this.redisCache = redisCache;
+	}
+
+	@Override
+	public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
+		if (delegate == null) {
+			delegate = new CompositeTokenGranter(getDefaultTokenGranters());
+		}
+		return delegate.grant(grantType, tokenRequest);
+	}
+
+	/**
+	 * 自定义授权模式
+	 */
+	private List<TokenGranter> getDefaultTokenGranters() {
+		ClientDetailsService clientDetails = clientDetailsService();
+		AuthorizationServerTokenServices tokenServices = tokenServices();
+		AuthorizationCodeServices authorizationCodeServices = authorizationCodeServices();
+		OAuth2RequestFactory requestFactory = requestFactory();
+
+		List<TokenGranter> tokenGranters = new ArrayList<>();
+		tokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetails, requestFactory));
+		tokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetails, requestFactory));
+		ImplicitTokenGranter implicit = new ImplicitTokenGranter(tokenServices, clientDetails, requestFactory);
+		tokenGranters.add(implicit);
+		tokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetails, requestFactory));
+		if (authenticationManager != null) {
+			tokenGranters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices, clientDetails, requestFactory));
+		}
+
+		// 自定义Granter
+		tokenGranters.add(new CaptchaTokenGranter(authenticationManager, tokenServices, clientDetails, requestFactory, redisCache));
+
+		return tokenGranters;
+	}
+
+	private ClientDetailsService clientDetailsService() {
+		BladeClientDetailsServiceImpl clientDetailsService = new BladeClientDetailsServiceImpl(dataSource);
+		clientDetailsService.setSelectClientDetailsSql(AuthConstant.DEFAULT_SELECT_STATEMENT);
+		clientDetailsService.setFindClientDetailsSql(AuthConstant.DEFAULT_FIND_STATEMENT);
+		return clientDetailsService;
+	}
+
+	private AuthorizationCodeServices authorizationCodeServices() {
+		return new InMemoryAuthorizationCodeServices();
+	}
+
+	private OAuth2RequestFactory requestFactory() {
+		return new DefaultOAuth2RequestFactory(clientDetailsService());
+	}
+
+	private DefaultTokenServices tokenServices() {
+		DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
+		defaultTokenServices.setTokenStore(tokenStore);
+		defaultTokenServices.setSupportRefreshToken(true);
+		TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
+		List<TokenEnhancer> enhancerList = new ArrayList<>();
+		enhancerList.add(jwtTokenEnhancer);
+		enhancerList.add(jwtAccessTokenConverter);
+		tokenEnhancerChain.setTokenEnhancers(enhancerList);
+		defaultTokenServices.setTokenEnhancer(tokenEnhancerChain);
+		defaultTokenServices.setClientDetailsService(clientDetailsService());
+		addUserDetailsService(defaultTokenServices, userDetailsService);
+		return defaultTokenServices;
+	}
+
+	private void addUserDetailsService(DefaultTokenServices tokenServices, UserDetailsService userDetailsService) {
+		if (userDetailsService != null) {
+			PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
+			provider.setPreAuthenticatedUserDetailsService(new UserDetailsByNameServiceWrapper<>(userDetailsService));
+			tokenServices.setAuthenticationManager(new ProviderManager(Collections.singletonList(provider)));
+		}
+	}
+
+}

+ 82 - 0
blade-auth/src/main/java/org/springblade/auth/granter/CaptchaTokenGranter.java

@@ -0,0 +1,82 @@
+package org.springblade.auth.granter;
+
+import org.springblade.auth.utils.TokenUtil;
+import org.springblade.common.cache.CacheNames;
+import org.springblade.core.redis.cache.BladeRedisCache;
+import org.springblade.core.tool.utils.StringUtil;
+import org.springblade.core.tool.utils.WebUtil;
+import org.springframework.security.authentication.*;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
+import org.springframework.security.oauth2.common.exceptions.UserDeniedAuthorizationException;
+import org.springframework.security.oauth2.provider.*;
+import org.springframework.security.oauth2.provider.token.AbstractTokenGranter;
+import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * 验证码TokenGranter
+ *
+ * @author Chill
+ */
+public class CaptchaTokenGranter extends AbstractTokenGranter {
+
+	private static final String GRANT_TYPE = "captcha";
+
+	private final AuthenticationManager authenticationManager;
+
+	private BladeRedisCache redisCache;
+
+	public CaptchaTokenGranter(AuthenticationManager authenticationManager,
+							   AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, BladeRedisCache redisCache) {
+		this(authenticationManager, tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
+		this.redisCache = redisCache;
+	}
+
+	protected CaptchaTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices,
+												ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, String grantType) {
+		super(tokenServices, clientDetailsService, requestFactory, grantType);
+		this.authenticationManager = authenticationManager;
+	}
+
+	@Override
+	protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
+		HttpServletRequest request = WebUtil.getRequest();
+		// 增加验证码判断
+		String key = request.getHeader(TokenUtil.CAPTCHA_HEADER_KEY);
+		String code = request.getHeader(TokenUtil.CAPTCHA_HEADER_CODE);
+		// 获取验证码
+		String redisCode = redisCache.get(CacheNames.CAPTCHA_KEY + key);
+		// 判断验证码
+		if (code == null || !StringUtil.equalsIgnoreCase(redisCode, code)) {
+			throw new UserDeniedAuthorizationException(TokenUtil.CAPTCHA_NOT_CORRECT);
+		}
+
+		Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters());
+		String username = parameters.get("username");
+		String password = parameters.get("password");
+		// Protect from downstream leaks of password
+		parameters.remove("password");
+
+		Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);
+		((AbstractAuthenticationToken) userAuth).setDetails(parameters);
+		try {
+			userAuth = authenticationManager.authenticate(userAuth);
+		}
+		catch (AccountStatusException | BadCredentialsException ase) {
+			//covers expired, locked, disabled cases (mentioned in section 5.2, draft 31)
+			throw new InvalidGrantException(ase.getMessage());
+		}
+		// If the username/password are wrong the spec says we should send 400/invalid grant
+
+		if (userAuth == null || !userAuth.isAuthenticated()) {
+			throw new InvalidGrantException("Could not authenticate user: " + username);
+		}
+
+		OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
+		return new OAuth2Authentication(storedOAuth2Request, userAuth);
+	}
+}

+ 33 - 0
blade-auth/src/main/java/org/springblade/auth/props/AuthProperties.java

@@ -0,0 +1,33 @@
+/*
+ *      Copyright (c) 2018-2028, Chill Zhuang All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are met:
+ *
+ *  Redistributions of source code must retain the above copyright notice,
+ *  this list of conditions and the following disclaimer.
+ *  Redistributions in binary form must reproduce the above copyright
+ *  notice, this list of conditions and the following disclaimer in the
+ *  documentation and/or other materials provided with the distribution.
+ *  Neither the name of the dreamlu.net developer nor the names of its
+ *  contributors may be used to endorse or promote products derived from
+ *  this software without specific prior written permission.
+ *  Author: Chill 庄骞 (smallchill@163.com)
+ */
+package org.springblade.auth.props;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+
+/**
+ * AuthProperties
+ *
+ * @author Chill
+ */
+@Data
+@RefreshScope
+@ConfigurationProperties(prefix = "blade.oauth")
+public class AuthProperties {
+
+}

+ 3 - 1
blade-auth/src/main/java/org/springblade/auth/utils/TokenUtil.java

@@ -48,7 +48,9 @@ public class TokenUtil {
 	public final static String LICENSE = TokenConstant.LICENSE;
 	public final static String LICENSE_NAME = TokenConstant.LICENSE_NAME;
 
-
+	public final static String CAPTCHA_HEADER_KEY = "Captcha-Key";
+	public final static String CAPTCHA_HEADER_CODE = "Captcha-Code";
+	public final static String CAPTCHA_NOT_CORRECT = "验证码不正确";
 	public final static String TENANT_HEADER_KEY = "Tenant-Id";
 	public final static String TENANT_PARAM_KEY = "tenant_id";
 	public final static String DEFAULT_TENANT_ID = "000000";

+ 1 - 1
blade-common/src/main/java/org/springblade/common/cache/CacheNames.java

@@ -23,6 +23,6 @@ package org.springblade.common.cache;
  */
 public interface CacheNames {
 
-	String NOTICE_ONE = "notice:one";
+	String CAPTCHA_KEY = "blade:auth::blade:captcha:";
 
 }

+ 1 - 0
blade-gateway/src/main/java/org/springblade/gateway/provider/AuthProvider.java

@@ -36,6 +36,7 @@ public class AuthProvider {
 	static {
 		defaultSkipUrl.add("/example");
 		defaultSkipUrl.add("/oauth/token/**");
+		defaultSkipUrl.add("/oauth/captcha/**");
 		defaultSkipUrl.add("/oauth/user-info");
 		defaultSkipUrl.add("/token/**");
 		defaultSkipUrl.add("/actuator/health/**");

+ 4 - 0
doc/sql/update/common-update-2.3.0~2.3.1.sql

@@ -0,0 +1,4 @@
+-- ----------------------------
+-- 增加验证码授权类型
+-- ----------------------------
+update blade_client set authorized_grant_types = 'refresh_token,password,authorization_code,captcha';