🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
# Spring Cloud下基于OAUTH2认证授权的实现示例 在`Spring Cloud`需要使用`OAUTH2`来实现多个微服务的统一认证授权,通过向`OAUTH服务`发送某个类型的`grant type`进行集中认证和授权,从而获得`token`,而这个token是受其他微服务信任的,我们在后续的访问可以通过`token`来进行,从而实现了微服务的统一认证授权。 **本示例提供了四大部分:** 1. `discovery-service`:服务注册和发现的基本模块 2. `auth-server`:OAUTH2认证授权中心 3. `order-service`:普通微服务,用来验证认证和授权 4. `api-gateway`:边界网关(所有微服务都在它之后) **OAUTH2中的角色:** 1. `Resource Server`:被授权访问的资源 2. `Authotization Server`:OAUTH2认证授权中心 3. `Resource Owner`: 用户 4. `Client`:使用API的客户端(如Android 、IOS、web app) Grant Type: 1. `Authorization Code`:用在服务端应用之间 2. `Implicit`:用在移动app或者web app(这些app是在用户的设备上的,如在手机上调起微信来进行认证授权) 3. `Resource Owner Password Credentials(password)`:应用直接都是受信任的(都是由一家公司开发的,本例子使用 4. `Client Credentials`:用在应用API访问。 5. 【扩展pwd】MobilePassword 手机号密码 6. 【扩展sms】MobileSms 手机号短信验证码 环境搭建: 1、基础环境 ``` mysql oauth\_client\_details 表存储客户端信息 redis 存储短信验证码信息 ``` 2、auth-server搭建 ***** \*\*2.1 \*\*定义拦截器拦截登录的请求【在这个类种主要完成2部分工作:1、根据参数获取当前的是认证类型,2、根据不同的认证类型调用不同的IntegrationAuthenticator.prepar进行预处理】 ~~~ package com.hjf.auth.filter; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; import org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * 功能说明:【】 * 作 者:lihaijun * 创建日期:2020-11-20 * Spring Security对于获取TOKEN的请求(默认是"/oauth/token"),需要认证client_id和client_secret。 * 认证client_id和client_secret可以有2种方式,一种是通过本节讲的ClientCredentialsTokenEndpointFilter, * 另一种是通过BasicAuthenticationFilter。 * ClientCredentialsTokenEndpointFilter首先比对请求URL是否是TOKEN请求路径以及请求参数中是否包含 * client_id,如果满足以上条件,再调用ProviderManager认证client_id和client_secret是否与配置的一致。 * 如果通过认证,会把身份认证信息保存打SecurityContext上下文中。 */ public class CustomClientCredentialsTokenEndpointFilter extends ClientCredentialsTokenEndpointFilter { private AuthorizationServerSecurityConfigurer configurer; private AuthenticationEntryPoint authenticationEntryPoint; public CustomClientCredentialsTokenEndpointFilter(AuthorizationServerSecurityConfigurer configurer, String path) { this.configurer = configurer; setFilterProcessesUrl(path); } @Override public void setAuthenticationEntryPoint(AuthenticationEntryPoint authenticationEntryPoint) { // 把父类的干掉 super.setAuthenticationEntryPoint(null); this.authenticationEntryPoint = authenticationEntryPoint; } @Override protected AuthenticationManager getAuthenticationManager() { return configurer.and().getSharedObject(AuthenticationManager.class); } @Override public void afterPropertiesSet() { setAuthenticationFailureHandler(new AuthenticationFailureHandler() { @Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { authenticationEntryPoint.commence(httpServletRequest, httpServletResponse, e); } }); setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { // 无操作-仅允许过滤器链继续到令牌端点 } }); } } ~~~ ***** **2.2 OAuth2服务配置 【将拦截器放入到拦截链条中】** ~~~ package com.hjf.auth.configurer; /** * 功能说明:【授权服务配置】 * 作 者:lihaijun * 创建日期:2020-11-09 */ @Configuration @EnableAuthorizationServer public class AuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter { @Autowired private TokenService tokenService; @Autowired private AuthenticationEntryPoint authenticationEntryPoint; @Autowired private CustomUserDetailsService userDetailsService; @Autowired private AuthorizationServerEndpointsConfiguration configuration; @Resource private DataSource dataSource; @Resource private AuthenticationManager authenticationManager; /** * 配置客户端详情 */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.withClientDetails(clientDetails()); } /** * 配置令牌端点得访问约束 */ @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { String path = "/oauth/token"; try { // 获取自定义映射路径,比如 ((AuthorizationServerEndpointsConfigurer) endpoints).pathMapping("/oauth/token", "/my/token"); path = configuration.oauth2EndpointHandlerMapping().getServletPath(path); } catch (Exception e) { } CustomClientCredentialsTokenEndpointFilter endpointFilter = new CustomClientCredentialsTokenEndpointFilter(security, path); endpointFilter.afterPropertiesSet(); endpointFilter.setAuthenticationEntryPoint(authenticationEntryPoint); security.authenticationEntryPoint(authenticationEntryPoint); security.addTokenEndpointAuthenticationFilter(endpointFilter); security.tokenKeyAccess("isAuthenticated()") .checkTokenAccess("permitAll()"); } /** * 配置令牌访问端点、令牌服务 */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.tokenStore(tokenService.tokenStore()); List<TokenGranter> tokenGranters = getTokenGranters(endpoints.getAuthorizationCodeServices(), endpoints.getTokenStore(), endpoints.getTokenServices(), endpoints.getClientDetailsService(), endpoints.getOAuth2RequestFactory()); endpoints.tokenGranter(new CompositeTokenGranter(tokenGranters)); endpoints.tokenEnhancer(tokenService.tokenEnhancer()); } /** * 授权模式【用户名密码、Token刷新、授权码、手机号密码、手机号验证码】 * @param authorizationCodeServices * @param tokenStore * @param tokenServices * @param clientDetailsService * @param requestFactory */ private List<TokenGranter> getTokenGranters(AuthorizationCodeServices authorizationCodeServices, TokenStore tokenStore, AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory) { return new ArrayList<>(Arrays.asList( new ResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices, clientDetailsService, requestFactory), new CustomRefreshTokenGranter(tokenStore, tokenServices, clientDetailsService, requestFactory), new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetailsService, requestFactory), new MobilePasswordCustomTokenGranter(userDetailsService, tokenServices, clientDetailsService, requestFactory), new MobileSmsCustomTokenGranter(userDetailsService, tokenServices, clientDetailsService, requestFactory) )); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public ClientDetailsService clientDetails() { return new JdbcClientDetailsService(dataSource); } } ~~~ ***** **2.3web安全配置** ~~~ package com.hjf.auth.configurer; import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; 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.WebSecurityConfigurerAdapter; /** * 功能说明:【安全配置】 * 作 者:lihaijun * 创建日期:2020-11-09 */ @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll() .antMatchers("/oauth/**").permitAll() .anyRequest().permitAll() .and() .csrf().disable(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } } ~~~ **2.4 自定义令牌授予者【手机号+验证码等】** ~~~ package com.hjf.auth.granter; import com.hjf.auth.model.MyUserDetails; import com.hjf.auth.service.CustomUserDetailsService; import org.springframework.security.oauth2.provider.ClientDetailsService; import org.springframework.security.oauth2.provider.OAuth2RequestFactory; import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices; import java.util.Map; /** * 功能说明:【自定义手机号+验证码令牌授予者】 * 作 者:lihaijun * 创建日期:2020-11-09 */ public class MobileSmsCustomTokenGranter extends AbstractCustomTokenGranter { protected CustomUserDetailsService userDetailsService; public MobileSmsCustomTokenGranter(CustomUserDetailsService userDetailsService, AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory) { super(tokenServices, clientDetailsService, requestFactory, "sms"); this.userDetailsService = userDetailsService; } @Override protected MyUserDetails getCustomUser(Map<String, String> parameters) { String mobile = parameters.get("mobile"); String smscode = parameters.get("smscode"); return userDetailsService.loadUserByMobileAndSmsCode(mobile, smscode); } } ~~~ **2.5 用户细节服务实现 【具体实现根据认证类型进行用户数据查询操作】** ~~~ package com.hjf.auth.service; import com.hjf.api.entity.Account; import com.hjf.auth.constant.CacheKeys; import com.hjf.auth.constant.MessageConstant; import com.hjf.auth.mapper.AccountMapper; import com.hjf.auth.model.MyUserDetails; import com.hjf.core.cache.RedisUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.common.exceptions.InvalidGrantException; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import javax.annotation.Resource; /** * 功能说明:【用户细节服务实现】 * 作 者:lihaijun * 创建日期:2020-11-20 */ @Slf4j @Service public class CustomUserDetailsService implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; @Resource RedisUtil redisUtil; @Resource AccountMapper accountMapper; /** * 手机号密码模式验证逻辑 */ public MyUserDetails loadUserByMobileAndPassword(String mobile, String password) { if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)) { throw new InvalidGrantException("您输入的手机号或密码不正确"); } Account account=accountMapper.getByTelephone(mobile); if (account==null){ throw new UsernameNotFoundException(MessageConstant.LOGIN_ERROR_ACCOUNT_NOTFOUND); } MyUserDetails userDetails=new MyUserDetails(account); return userDetails; } /** * 手机号和验证码验证逻辑 */ public MyUserDetails loadUserByMobileAndSmsCode(String mobile, String smsCode) { if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(smsCode)) { log.error("【根据手机号和验证码查询用户信息】 缺失手机号或者验证码"); throw new InvalidGrantException(MessageConstant.LOGIN_ERROR_PHONE_OR_SMSCODE); } Object smsCodeCache=redisUtil.get(CacheKeys.SMS_CODE+mobile); if (smsCodeCache==null){ log.error("【根据手机号和验证码查询用户信息】 没有查询到缓存数据"); throw new InvalidGrantException(MessageConstant.LOGIN_ERROR_PHONE_OR_SMSCODE); } if (!smsCode.equals(smsCodeCache.toString())){ log.error("【根据手机号和验证码查询用户信息】 缓存查询到的信息和输入信息不匹配"); throw new InvalidGrantException(MessageConstant.LOGIN_ERROR_PHONE_OR_SMSCODE); } Account account=accountMapper.getByTelephone(mobile); if (account==null){ log.error("【根据手机号和验证码查询用户信息】 数据库没有查询到账户信息"); throw new UsernameNotFoundException(MessageConstant.LOGIN_ERROR_ACCOUNT_NOTFOUND); } MyUserDetails userDetails=new MyUserDetails(account); return userDetails; } /** * 用户名&密码验证逻辑【系统ResourceOwnerPasswordTokenGranter使用的默认功能 】 */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { String[] permissionArray = new String[]{}; Account account=accountMapper.getByTelephone(username); if (account==null){ log.error("【用户名&密码认证】"); throw new UsernameNotFoundException(MessageConstant.USERNAME_PASSWORD_ERROR); } MyUserDetails securityUser = new MyUserDetails(account); securityUser.setPassword(passwordEncoder.encode(account.getPassword())); securityUser.setEnabled(true); return securityUser; } } ~~~ **2.6 token服务工具【tokenEnhancer、tokenStore、jwtAccessTokenConverter】** ~~~ package com.hjf.auth.token; import com.hjf.auth.model.MyUserDetails; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cglib.beans.BeanMap; import org.springframework.context.annotation.Bean; import org.springframework.core.io.ClassPathResource; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.oauth2.common.DefaultExpiringOAuth2RefreshToken; import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; import org.springframework.security.oauth2.common.DefaultOAuth2RefreshToken; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter; import org.springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter; import org.springframework.security.oauth2.provider.token.TokenEnhancer; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore; import org.springframework.security.rsa.crypto.KeyStoreKeyFactory; import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import java.security.KeyPair; import java.util.Collection; import java.util.LinkedHashMap; import java.util.Map; import java.util.UUID; /** * 功能说明:【Token服务】 * 作 者:lihaijun * 创建日期:2020-11-20 */ @Component public class TokenService { @Autowired private RedisConnectionFactory redisFactory; private TokenStoreType tokenStoreType = TokenStoreType.JWT; private enum TokenStoreType { IN_MEMORY, REDIS, JWT } @Bean public TokenStore tokenStore() { switch (tokenStoreType) { case REDIS: return new RedisTokenStore(redisFactory); case JWT: return new JwtTokenStore(jwtAccessTokenConverter()); } return new InMemoryTokenStore(); } @Bean public TokenEnhancer tokenEnhancer() { return new TokenEnhancer() { @Override public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) { DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) oAuth2AccessToken; MyUserDetails user = (MyUserDetails) oAuth2Authentication.getPrincipal(); Map<String, Object> map = new LinkedHashMap<>(); map.put("username", user.getUsername()); map.put("telephone", user.getTelephone()); map.put("accountId", user.getAccountId()); token.setAdditionalInformation(map); if (tokenStoreType == TokenStoreType.JWT) { token = (DefaultOAuth2AccessToken) jwtAccessTokenConverter().enhance(oAuth2AccessToken, oAuth2Authentication); } else { token.setValue(buildTokenValue()); if (token.getRefreshToken() != null) { if (token.getRefreshToken() instanceof DefaultExpiringOAuth2RefreshToken) { DefaultExpiringOAuth2RefreshToken refreshToken = (DefaultExpiringOAuth2RefreshToken) token.getRefreshToken(); token.setRefreshToken(new DefaultExpiringOAuth2RefreshToken(buildTokenValue(), refreshToken.getExpiration())); } else { token.setRefreshToken(new DefaultOAuth2RefreshToken(buildTokenValue())); } } } return token; } }; } @Bean public KeyPair keyPair() { //从classpath下的证书中获取秘钥对 KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray()); return keyStoreKeyFactory.getKeyPair("jwt", "123456".toCharArray()); } @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter(); // accessTokenConverter.setSigningKey("demo-oauth2"); ((DefaultAccessTokenConverter) accessTokenConverter.getAccessTokenConverter()).setUserTokenConverter(new DefaultUserAuthenticationConverter() { @Override public Authentication extractAuthentication(Map<String, ?> map) { MyUserDetails customUser = new MyUserDetails(); BeanMap.create(customUser).putAll(map); Object authorities = map.get("authorities"); if (authorities instanceof String) { customUser.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList((String) authorities)); } else if (authorities instanceof Collection) { customUser.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils.collectionToCommaDelimitedString((Collection) authorities))); } return new PreAuthenticatedAuthenticationToken(customUser, null, customUser.getAuthorities()); } }); accessTokenConverter.setKeyPair(keyPair()); return accessTokenConverter; } public String buildTokenValue() { String tokenValue = UUID.randomUUID().toString() + UUID.randomUUID().toString(); return tokenValue; } } ~~~ **2.7 token生成入口** ~~~ package com.hjf.auth.controller; import com.hjf.auth.token.Oauth2TokenDto; import com.hjf.frame.base.BaseResp; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.annotation.*; import java.security.Principal; import java.util.Map; /** * 功能说明:【认证服务入口】 * 作 者:lihaijun * 创建日期:2020-11-20 */ @RestController @RequestMapping("/oauth") public class OauthController { @Autowired private TokenEndpoint tokenEndpoint; @GetMapping("/token") public BaseResp getAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException { return resp(tokenEndpoint.getAccessToken(principal, parameters).getBody()); } @PostMapping("/token") public BaseResp postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException { return resp(tokenEndpoint.postAccessToken(principal, parameters).getBody()); } /** * 定制申请返回实体 */ private BaseResp resp(OAuth2AccessToken accessToken) { Oauth2TokenDto oauth2TokenDto = Oauth2TokenDto.builder() .token(accessToken.getValue()) .refreshToken(accessToken.getRefreshToken().getValue()) .expiresIn(accessToken.getExpiresIn()) .tokenHead("Bearer ") .additionalInformation(accessToken.getAdditionalInformation()) .build(); return BaseResp.success(oauth2TokenDto); } } ~~~ ***** 测试 localhost:8000/oauth/token?client\_id=client-mp&client\_secret=123456&mobile=15010123237&password=123456&smscode=1111&grant\_type=sms ![](https://img.kancloud.cn/9c/45/9c45e725c73882e3149bdd6904f90e9a_735x523.png)