关于OAuth2的解释太少了,这玩意儿困扰我太久了
1.这玩意儿是什么
这玩意儿,不好理解,特别是我这种android开发,接触这个,我觉得太难了,太绕了,我的理解更多的是,既然要做校验,为什么不直接写拦截器呢?拦截器不是更加简单易懂吗?
2.这玩意儿怎么运转的
这个真的不好解释,假设,我有一个登录服务,暂且称LoginService
- LoginService 端口90000
@PostMapping("/login")
@ApiOperation(value = "后台管理员登录")
@ApiImplicitParams(
{
@ApiImplicitParam(name = "loginParam", value = " 登录参数")
}
)
public Result<LoginMemberVO> login(@RequestBody LoginParam loginParam) {
return Result.buildSuccess(memberLoginService.login(loginParam));
}
- 我模拟登录的时候,就可以使用
http://localhost:90000/login
- 然后就是账号密码一顿输入,这个时候,密码怎么破?都是明文啊,数据库保存也都是明文啊,于是乎,spring-security-oauth2登场
2.首先引库
<!-- oauth2鉴权依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
3.然后自定义OAuth2校验,这个地方卡了我3天啊!这里建议用Feign调用
//调用远程auth服务获取token
@FeignClient(value = "zero-authentication")
public interface OAuth2FeignClient {
@PostMapping("/oauth/token")
ResponseEntity<JwtToken> getToken(
@RequestParam("grant_type") String grantType , // 授权类型
@RequestParam("username") String username , // 用户名
@RequestParam("password") String password , // 用户的密码
@RequestParam("LOGIN_TYPE") String loginType, // 登录的类型
@RequestHeader("Authorization") String basicToken // Basic 由第三方客户端信息加密出现的值
) ;
}
4.在具体登录的地方进行oauth2的调用
public LoginMemberVO login(LoginParam loginParam) {
LoginMemberVO loginMemberVO = null;
//调用远程auth服务获取token
ResponseEntity<JwtToken> tokenResponseEntity = oAuth2FeignClient.getToken("password", loginParam.getUsername(), loginParam.getPassword(), "MEMBER_TYPE", basicToken);
if (tokenResponseEntity.getStatusCode() == HttpStatus.OK) {
JwtToken jwtToken = tokenResponseEntity.getBody();
if(jwtToken == null) {
ExceptionCatcher.catchAuthFailEx();
}
//构建返回结果
loginMemberVO = new LoginMemberVO(loginParam.getUsername(), jwtToken.getExpiresIn(), jwtToken.getTokenType() + " " + jwtToken.getAccessToken(), jwtToken.getRefreshToken());
//保存token到redis
redisUtils.setEx(RedisPrefixConstants.JWT_MEMBER_PREFIX + jwtToken.getAccessToken(), "", jwtToken.getExpiresIn(), TimeUnit.SECONDS);
}
return loginMemberVO;
}
5.请注意以下代码
ResponseEntity<JwtToken> tokenResponseEntity = oAuth2FeignClient.getToken("password", loginParam.getUsername(), loginParam.getPassword(), "MEMBER_TYPE", basicToken);
- 这句是干嘛的?为啥就能返回数据了?就是这里卡了我3天,我完全蒙蔽,这就一个Feign的远程调用,我似乎找不到哪里定义了这么一个接口。
- 看源码TokenEndpoint
@RequestMapping(
value = {"/oauth/token"},
method = {RequestMethod.GET}
)
public ResponseEntity<OAuth2AccessToken> getAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
if (!this.allowedRequestMethods.contains(HttpMethod.GET)) {
throw new HttpRequestMethodNotSupportedException("GET");
} else {
return this.postAccessToken(principal, parameters);
}
}
@RequestMapping(
value = {"/oauth/token"},
method = {RequestMethod.POST}
)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
if (!(principal instanceof Authentication)) {
throw new InsufficientAuthenticationException("There is no client authentication. Try adding an appropriate authentication filter.");
} else {
String clientId = this.getClientId(principal);
ClientDetails authenticatedClient = this.getClientDetailsService().loadClientByClientId(clientId);
TokenRequest tokenRequest = this.getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
if (clientId != null && !clientId.equals("") && !clientId.equals(tokenRequest.getClientId())) {
throw new InvalidClientException("Given client ID does not match authenticated client");
} else {
if (authenticatedClient != null) {
this.oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
}
if (!StringUtils.hasText(tokenRequest.getGrantType())) {
throw new InvalidRequestException("Missing grant type");
} else if (tokenRequest.getGrantType().equals("implicit")) {
throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
} else {
if (this.isAuthCodeRequest(parameters) && !tokenRequest.getScope().isEmpty()) {
this.logger.debug("Clearing scope of incoming token request");
tokenRequest.setScope(Collections.emptySet());
}
if (this.isRefreshTokenRequest(parameters)) {
tokenRequest.setScope(OAuth2Utils.parseParameterList((String)parameters.get("scope")));
}
OAuth2AccessToken token = this.getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
} else {
return this.getResponse(token);
}
}
}
}
}
- 没错,就是这里了,这里定义了这个oauth的调用,我们继续看源码
- 到这里,我们带着问题
1.首先,我如何登陆,我输入的账号密码,oauth2,做了什么进行匹配呢?
这里可以肯定的是,远程服务端的数据库保存的密码,绝对不会是明文的,所以,必然有某种加密规则把远程的数据,跟oauth2进行对比。
以下流程十分重要,一定要认真对着源码看下去
- TokenEndpoint类源码的这一句,可以看出是进行token的获取
OAuth2AccessToken token = this.getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
- 会调用CompositeTokenGranter的grant方法
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
Iterator var3 = this.tokenGranters.iterator();
OAuth2AccessToken grant;
do {
if (!var3.hasNext()) {
return null;
}
TokenGranter granter = (TokenGranter)var3.next();
//主要看这句了
grant = granter.grant(grantType, tokenRequest);
} while(grant == null);
return grant;
}
- AbstractTokenGranter 接着调用grant
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
if (!this.grantType.equals(grantType)) {
return null;
} else {
String clientId = tokenRequest.getClientId();
ClientDetails client = this.clientDetailsService.loadClientByClientId(clientId);
this.validateGrantType(grantType, client);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Getting access token for: " + clientId);
}
return this.getAccessToken(client, tokenRequest);
}
}
protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
return this.tokenServices.createAccessToken(this.getOAuth2Authentication(client, tokenRequest));
}
- ResourceOwnerPasswordTokenGranter的方法中getOAuth2Authentication
userAuth = this.authenticationManager.authenticate(userAuth);
- 走到ProviderManager的authenticate(Authentication authentication)方法体
result = provider.authenticate(authentication);
- 又走到 AbstractUserDetailsAuthenticationProvider的authenticate方法体
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
- 又又又走到DaoAuthenticationProvider retrieveUser中
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
- 最终走到loadUserByUsername
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
关键的地方来了,走了这么一大圈,为了做毛???就是为了,获取我们的密码,然后进行加密,最后跟远程数据库的加密后的密码进行对比
- 所以,我们一般会实现UserDetailsService,进行各种输出
@Service
public class CustomizeUserDetailsService implements UserDetailsService {
@Autowired
private SysUserRepository sysUserRepository;
@Autowired
private SysPrivilegeRepository sysPrivilegeRepository;
@Autowired
private MemberRepository memberRepository;
@Override
public UserDetails loadUserByUsername(String username) {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if(requestAttributes == null) {
throw new AuthenticationServiceException(LoginConstant.LOGIN_TYPE + "参数不存在");
}
String loginType = requestAttributes.getRequest().getParameter(LoginConstant.LOGIN_TYPE);
if(StringUtils.isBlank(loginType)) {
throw new AuthenticationServiceException(LoginConstant.LOGIN_TYPE + "参数不存在");
}
UserDetails userDetails = null;
switch (loginType) {
case LoginConstant.ADMIN_TYPE:
userDetails = loadAdminByUsername(username);
break;
case LoginConstant.MEMBER_TYPE:
userDetails = loadMemberByUsername(username);
break;
default:
throw new AuthenticationServiceException("暂不支持的登录方式: " + loginType);
}
return userDetails;
}
private UserDetails loadMemberByUsername(String username) {
Member member = memberRepository.findByUsernameOrPhoneOrEmailAndStatus(username, username, username, 1);
return new User(
String.valueOf(member.getId()),
member.getPassword(),
true,
true,
true,
true,
Arrays.asList(new SimpleGrantedAuthority("ROLE_MEMBER"))
);
}
private UserDetails loadAdminByUsername(String username) {
SysUser sysUser = sysUserRepository.findByUsernameAndStatus(username, 1);
if(ObjectUtils.isNotEmpty(sysUser)) {
Collection authorities;
User user = new User(String.valueOf(sysUser.getId()),
sysUser.getPassword(),
true,
true,
true,
true,
getSysUserPermissions(sysUser.getId()));
return user;
}
return null;
}
/**
* 通過用户ID获取用户权限
* @param userId
* @return
*/
private Set<SimpleGrantedAuthority> getSysUserPermissions(Long userId) {
String roleCode = sysUserRepository.getRoleCode(userId);
List<SysPrivilege> privileges;
if(ADMIN_ROLE_CODE.equals(roleCode)) {
privileges = sysPrivilegeRepository.findAll();
}else {
privileges = sysPrivilegeRepository.findByUserId(userId);
}
if(ObjectUtils.isEmpty(privileges)) {
return Collections.emptySet();
}
return privileges
.stream()
.distinct()
.map( privilege -> new SimpleGrantedAuthority(privilege.getName()))
.collect(Collectors.toSet());
}
}
到这里一切似乎十分的美好,然后哪里做密码的校验呢???oauth2 是真的绕,它提供了几种加密的方案,是可以配置的,所以配置在哪里呢??这个时候
WebSecurityConfigurerAdapter登场了,我们需要继承WebSecurityConfigurerAdapter进行自定义了
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManager();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable(); //关闭csrf
http.authorizeRequests().anyRequest().authenticated();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
- BCryptPasswordEncoder 这个玩意儿就是我们配置的加密玩意儿,通过源码可知,里面有encode跟match方法
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("rawPassword cannot be null");
} else if (encodedPassword != null && encodedPassword.length() != 0) {
if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
this.logger.warn("Encoded password does not look like BCrypt");
return false;
} else {
return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
}
} else {
this.logger.warn("Empty encoded password");
return false;
}
}
public String encode(CharSequence rawPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("rawPassword cannot be null");
} else {
String salt;
if (this.random != null) {
salt = BCrypt.gensalt(this.version.getVersion(), this.strength, this.random);
} else {
salt = BCrypt.gensalt(this.version.getVersion(), this.strength);
}
return BCrypt.hashpw(rawPassword.toString(), salt);
}
}
- 简单理解,就是,我传入了密码(admin),然后通过encode加密后,获取一串字符串,这个字符串再根据match,把远程的密码,跟encode后的字符串进行对比判断密码是否正确
那么问题又来了,什么时候encode呢?AuthorizationServerConfigurerAdapter这个玩意儿就是做这些配置的初始化的
@EnableAuthorizationServer
@Configuration
public class AuthenticationConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthenticationManager authenticationManager;
@Qualifier("customizeUserDetailsService")
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("zero-mall")
.secret(passwordEncoder.encode("zero-mall-secret"))
.scopes("all")
.authorizedGrantTypes("password", "refresh_token")
.accessTokenValiditySeconds(24 * 7200)
.refreshTokenValiditySeconds(7 * 24 * 7200)
.and()
.withClient("inside-app")
.secret(passwordEncoder.encode("inside-secret"))
.authorizedGrantTypes("client_credentials")
.scopes("all")
.accessTokenValiditySeconds(7 * 24 *3600);
super.configure(clients);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService)
.tokenStore(jwtTokenStore())
.tokenEnhancer(jwtAccessTokenConverter());
super.configure(endpoints);
}
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter tokenConverter = new JwtAccessTokenConverter();
ClassPathResource classPathResource = new ClassPathResource("zero-mall.jks");
// 获取KeyStoreFactory
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(classPathResource, "123456".toCharArray()) ;
tokenConverter.setKeyPair(keyStoreKeyFactory.getKeyPair("zero-mall", "123456".toCharArray()));
return tokenConverter;
}
}
- 不用太在意里面写了什么,关键是流程,贴出来的代码都可以自定义
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("zero-mall")
.secret(passwordEncoder.encode("zero-mall-secret"))
.scopes("all")
.authorizedGrantTypes("password", "refresh_token")
.accessTokenValiditySeconds(24 * 7200)
.refreshTokenValiditySeconds(7 * 24 * 7200)
.and()
.withClient("inside-app")
.secret(passwordEncoder.encode("inside-secret"))
.authorizedGrantTypes("client_credentials")
.scopes("all")
.accessTokenValiditySeconds(7 * 24 *3600);
super.configure(clients);
}
- 这里初始化了 .secret(passwordEncoder.encode("zero-mall-secret"))配置,然后就是一顿操作了
最后,其实这里还有一个类 ResourceServerConfigurerAdapter,我暂时没有用到,但是通过源码我们可以知道,也是配置
public void configure(HttpSecurity http) throws Exception {
((AuthorizedUrl)http.authorizeRequests().anyRequest()).authenticated();
}
- 这个配置告诉我们,所有的请求都需要经过oauth2的认证,但是这一步,一般我们都是放网关层(getway)处理掉了
网友评论