前言
项目基于springcloud,授权决定使用提供的spring-oauth2,了解了oauth2原理之后,基于业务有很多需要重写的点;
1.获取token同时需要获取客户端信息(手机型号,app版本号等)
2.除了提供的password方式登录,还需要提供验证码登录功能
源码理解
对spring-cloud-starter-oauth2包的源码进行了简单的了解:
endpoint: 端点,/oauth/token的接口地址 TokenEndPoint是接口的逻辑
TokenGranter:token授权,默认提供了4种granter(RefreshTokenGranter,ImplicitTokenGranter,ResourceOwnerPasswordTokenGranter,ClientCredentialsTokenGranter)
自定义TokenGranter可以实现获取除了默认参数的其他参数,同时可以增加验证
AuthenticationProvider:身份验证提供者,用于验证身份,密码的匹配等都是再这通过service获取用户信息进行校验
DaoAuthenticationProvider是默认的实现类,通过重写provider来自定义校验逻辑
UserDetailsService:用户信息查询
实现
第一步:指定自定义provider
自定义granter
package com.mlines.cloud.granter;
import com.mlines.cloud.token.AppAuthenticationToken;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AccountStatusException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
import org.springframework.security.oauth2.provider.*;
import org.springframework.security.oauth2.provider.token.AbstractTokenGranter;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 短信验证码方式
* zhangmx
*/
public class SMSCodeTokenGranter extends AbstractTokenGranter {
private static final String GRANT_TYPE = "smscode";
private final AuthenticationManager authenticationManager;
public SMSCodeTokenGranter(AuthenticationManager authenticationManager,
AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory) {
this(authenticationManager, tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
}
protected SMSCodeTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices,
ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, String grantType) {
super(tokenServices, clientDetailsService, requestFactory, grantType);
this.authenticationManager = authenticationManager;
}
/**
* 在这个方法可以进行验证码等其他操作
* @param client
* @param tokenRequest
* @return
*/
@Override
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters());
String username = parameters.get("username");
String smscode = parameters.get("smscode");
String deviceId=parameters.get("deviceId");
String type=parameters.get("type");
String sysVersion=parameters.get("sysVersion");
String deviceVersion=parameters.get("deviceVersion");
String appVersion=parameters.get("appVersion");
parameters.put("loginType","sms");
// Protect from downstream leaks of password
parameters.remove("password");
//校验验证码
if(!StringUtils.equals(smscode,"1001")){
throw new InvalidGrantException("验证码不正确");
}
Authentication userAuth = new AppAuthenticationToken(username, smscode,type,deviceId,sysVersion,deviceVersion,appVersion);
((AbstractAuthenticationToken) userAuth).setDetails(parameters);
try {
userAuth = authenticationManager.authenticate(userAuth);
}
catch (AccountStatusException ase) {
//covers expired, locked, disabled cases (mentioned in section 5.2, draft 31)
throw new InvalidGrantException(ase.getMessage());
}
catch (BadCredentialsException e) {
// If the username/password are wrong the spec says we should send 400/invalid grant
throw new InvalidGrantException(e.getMessage());
}
if (userAuth == null || !userAuth.isAuthenticated()) {
throw new InvalidGrantException("Could not authenticate user: " + username);
}
OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, userAuth);
}
}
第二步:指定自定义provider
自定义provider:
package com.mlines.cloud.provider;
import com.mlines.cloud.feign.client.SysUserClient;
import com.mlines.cloud.token.AppAuthenticationToken;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
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.util.Assert;
import java.util.Map;
/**
* Created by fp295 on 2018/11/25.4
* 用户名密码登录
*/
public class AppAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private SysUserClient sysUserClient;
private PasswordEncoder passwordEncoder;
// /**
// * 根据用户名取回用户信息后,进入此方法判断密码是否匹配,如果是验证码登录 判断验证码是否正确
// * @param var1
// * @param authentication
// * @throws AuthenticationException
// */
// @Override
// protected void additionalAuthenticationChecks(UserDetails var1, Authentication authentication) throws AuthenticationException {
//
// if(authentication.getCredentials() == null) {
// this.logger.debug("Authentication failed: no credentials provided");
// throw new BadCredentialsException(this.messages.getMessage("AppAuthenticationProvider.badCredentials", "Bad credentials"));
// } else {
// String presentedPassword = authentication.getCredentials().toString();
// Map<String,Object> otherParams=(Map<String,Object>)authentication.getDetails();//其他参数
// String loginType=(String)otherParams.get("loginType");
// if(StringUtils.equals(loginType,"password")){//密码方式登录
// if (!passwordEncoder.matches(presentedPassword, var1.getPassword())) {
// logger.debug("密码错误");
//
// throw new BadCredentialsException(messages.getMessage(
// "400",
// "密码错误"));
// }
// }
//
// 验证码验证,调用公共服务查询 key 为authentication.getPrincipal()的value, 并判断其与验证码是否匹配
if(!"1000".equals(presentedPassword)){
this.logger.debug("Authentication failed: verifyCode does not match stored value");
throw new BadCredentialsException(this.messages.getMessage("AppAuthenticationProvider.badCredentials", "Bad verifyCode"));
}
// }
// }
//
//
//
// @Override
// protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user,String deviceId) {
// AppAuthenticationToken result = new AppAuthenticationToken(principal, authentication.getCredentials(), user.getAuthorities(),deviceId);
// result.setDetails(authentication.getDetails());
// return result;
// }
//
// @Override
// protected UserDetails retrieveUser(String phone, Authentication authentication) throws AuthenticationException {
// UserDetails loadedUser;
// try {
// loadedUser = userDetailsService.loadUserByUsername(phone);
// } catch (UsernameNotFoundException var6) {
// throw var6;
// } catch (Exception var7) {
// throw new InternalAuthenticationServiceException(var7.getMessage(), var7);
// }
//
// if(loadedUser == null) {
// throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
// } else {
// return loadedUser;
// }
// }
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if(authentication.getCredentials() == null) {
this.logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(this.messages.getMessage("AppAuthenticationProvider.badCredentials", "Bad credentials"));
} else {
String presentedPassword = authentication.getCredentials().toString();
Map<String,Object> otherParams=(Map<String,Object>)authentication.getDetails();//其他参数
String loginType=(String)otherParams.get("loginType");
if(StringUtils.equals(loginType,"password")){//密码方式登录
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("密码错误");
throw new BadCredentialsException(messages.getMessage(
"400",
"密码错误"));
}
}
//
// // 验证码验证,调用公共服务查询 key 为authentication.getPrincipal()的value, 并判断其与验证码是否匹配
// if(!"1000".equals(presentedPassword)){
// this.logger.debug("Authentication failed: verifyCode does not match stored value");
// throw new BadCredentialsException(this.messages.getMessage("AppAuthenticationProvider.badCredentials", "Bad verifyCode"));
// }
}
}
@Override
protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
UserDetails loadedUser;
try {
loadedUser = userDetailsService.loadUserByUsername(username);
} catch (UsernameNotFoundException var6) {
throw var6;
} catch (Exception var7) {
throw new InternalAuthenticationServiceException(var7.getMessage(), var7);
}
if(loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
} else {
return loadedUser;
}
}
@Override
public boolean supports(Class<?> authentication) {
return AppAuthenticationToken.class.isAssignableFrom(authentication);
}
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
this.passwordEncoder = passwordEncoder;
}
protected PasswordEncoder getPasswordEncoder() {
return passwordEncoder;
}
//
//
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
}
自定义service
其他代码片段
package com.mlines.cloud.config;
import com.mlines.cloud.granter.AppResourceOwnerPasswordTokenGranter;
import com.mlines.cloud.granter.SMSCodeTokenGranter;
import com.mlines.cloud.handler.CustomWebResponseExceptionTranslator;
import com.mlines.cloud.provider.AppAuthenticationProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
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.ClientDetailsService;
import org.springframework.security.oauth2.provider.CompositeTokenGranter;
import org.springframework.security.oauth2.provider.OAuth2RequestFactory;
import org.springframework.security.oauth2.provider.TokenGranter;
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.implicit.ImplicitTokenGranter;
import org.springframework.security.oauth2.provider.refresh.RefreshTokenGranter;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
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 org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Configuration
@EnableAuthorizationServer
public class OAuthConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private CustomAccessDeniedHandler customAccessDeniedHandler;
@Autowired
private CustomWebResponseExceptionTranslator customWebResponseExceptionTranslator;
// @Autowired
// private UsernameUserDetailService userDetailsService;
@Autowired
private UserDetailsService userDetailsService;//读取客户端的service app可以是一个客户端 web可以是一个客户端
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("web-app")
.secret( new BCryptPasswordEncoder().encode("rq9nuNkIwT"))
.scopes("web-app")
.authorizedGrantTypes("password","smscode", "refresh_token")
.accessTokenValiditySeconds(86400)//超时
.refreshTokenValiditySeconds(604800)
.and()
.withClient("android-app")
.secret(new BCryptPasswordEncoder().encode("ijnuybdsvv"))
.scopes("android-app")
.authorizedGrantTypes("password","smscode", "refresh_token")
.accessTokenValiditySeconds(86400*7)
.refreshTokenValiditySeconds(604800*4);
}
@Bean
public UserDetailsService userDetailsService(){
return userDetailsService;
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey("cloudserver");
return jwtAccessTokenConverter;
}
public static void main(String[] args){
// Jwt jwt=JwtHelper.decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtc2ciOiLnmbvlvZXmiJDlip8iLCJjb2RlIjoyMDAsImRhdGEiOnsibGFiZWwiOiLlvKDlrablj4siLCJpZCI6Mn0sInVzZXJfbmFtZSI6IjE2NjY2NjY2NjIxIiwic2NvcGUiOlsid2ViLWFwcCJdLCJleHAiOjE1NTA3NDc1NTQsImF1dGhvcml0aWVzIjpbIlJPTEVfVVNFUiJdLCJqdGkiOiJhZDk1OGZhMC1mYWNiLTQyOGQtYWZlMi0xNGQ0OGJhMTBjMjAiLCJjbGllbnRfaWQiOiJ3ZWItYXBwIn0.yQbgx8rXCkwqdYWhGOBIRJZyIe9VOKrV3yNIcqVE294");
// System.out.println(new String(jwt.getClaims().getBytes()));
System.out.println(new BCryptPasswordEncoder().encode("123456"));
System.out.println(new BCryptPasswordEncoder().matches("123456","$2a$10$xT8XiML30Yer0d3cYBmXfewMgZtQyiGc25zZs9ipkvKJuyOQDwcF2"));
}
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
enhancerChain.setTokenEnhancers(Arrays.asList(customTokenEnhancer(), jwtAccessTokenConverter()));
endpoints.tokenStore(jwtTokenStore())
.accessTokenConverter(jwtAccessTokenConverter())
.reuseRefreshTokens(false)//该字段设置设置refresh token是否重复使用,true:reuse;false:no reuse.
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService)
.tokenEnhancer(enhancerChain);
endpoints.exceptionTranslator(customWebResponseExceptionTranslator);
endpoints.tokenGranter(new CompositeTokenGranter(getTokenGranters(endpoints)));
}
private List<TokenGranter> getTokenGranters(AuthorizationServerEndpointsConfigurer endpoints){
ClientDetailsService clientDetails = endpoints.getClientDetailsService();
AuthorizationServerTokenServices tokenServices = endpoints.getTokenServices();
AuthorizationCodeServices authorizationCodeServices = endpoints.getAuthorizationCodeServices();
OAuth2RequestFactory requestFactory = endpoints.getOAuth2RequestFactory();
// ((DefaultTokenServices)tokenServices).setAuthenticationManager(new ProviderManager(getProvider(),null));
List<TokenGranter> tokenGranters = new ArrayList<TokenGranter>();
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));//有了自定义的用户名密码验证方式不需要初始化默认的了
tokenGranters.add(new AppResourceOwnerPasswordTokenGranter(new ProviderManager(getProvider(),null),tokenServices,endpoints.getClientDetailsService(),endpoints.getOAuth2RequestFactory()));
tokenGranters.add(new SMSCodeTokenGranter(new ProviderManager(getProvider(),null),tokenServices,endpoints.getClientDetailsService(),endpoints.getOAuth2RequestFactory()));
}
//添加自定义granter
return tokenGranters;
}
private List<AuthenticationProvider> getProvider(){
List<AuthenticationProvider> list=new ArrayList<>();
AppAuthenticationProvider provider = new AppAuthenticationProvider();
// 设置userDetailsService
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(new BCryptPasswordEncoder());
// 禁止隐藏用户未找到异常
provider.setHideUserNotFoundExceptions(false);
list.add(provider);
return list;
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients()
.authenticationEntryPoint(new AuthExceptionEntryPoint())//自定义异常信息
.accessDeniedHandler(customAccessDeniedHandler)
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
@Bean
public TokenEnhancer customTokenEnhancer() {
return new CustomTokenEnhancer();// 写入自定义信息
}
//
// @Override
// public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
// oauthServer.authenticationEntryPoint(new AuthExceptionEntryPoint());
// }
}
package com.mlines.cloud.config;
import com.mlines.cloud.filter.AppLoginAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
//
@Autowired
private UserDetailsService userDetailsService;
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// @Override
// protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// auth
// .userDetailsService(userDetailsService())
// .passwordEncoder(passwordEncoder());
// }
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//TODO:use md5
// auth.authenticationProvider(appAuthenticationProvider());
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
//不定义没有password grant_type
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
//
@Bean
public AppLoginAuthenticationFilter getPhoneLoginAuthenticationFilter() {
AppLoginAuthenticationFilter filter = new AppLoginAuthenticationFilter();
try {
filter.setAuthenticationManager(this.authenticationManagerBean());
} catch (Exception e) {
e.printStackTrace();
}
// filter.setAuthenticationSuccessHandler(new LoginAuthSuccessHandler());
return filter;
}
// /**
// * 手机验证码登陆过滤器
// * @return
// */
// @Bean
// public UserNamePwdAuthenticationFilter getUserNamePwdFilter() {
// UserNamePwdAuthenticationFilter filter = new UserNamePwdAuthenticationFilter();
// try {
// filter.setAuthenticationManager(this.authenticationManagerBean());
// } catch (Exception e) {
// e.printStackTrace();
// }
// return filter;
// }
}