OAuth 2.0为OAuth的协议的延续版本,是一个关于授权的开放网络标准,在全世界得到广泛应用。基于OAuth 2.0授权有一下几个特点:1、安全:平台商无需使用用户的用户名与密码就可以申请获得该用户资源的授权;2、开放:任何消费方都可以使用 OAuth 认证服务,任何服务提供方 ( Service Provider) 都可以实现自身的 OAuth 认证服务。3、简单:不管是消费方还是服务提供方,都很容易于理解与使用。OAuth 2.0认证流程如下:
provider为服务提供方,即第三方服务系统,主要用作管理用户资源和为其他系统提供认证服务。例如:我们使用的QQ账号是在腾讯的服务器上,腾讯提供统一对外接口给各个平台(如:今日头条),用户下载今日头条app后,就可以选择使用QQ登录今日头条app。用户的个人信息是存放于腾讯服务器上的,用户可以授权今日头条获取自己在腾讯服务器上的个人信息,今日头条获取到用户信息后存于自己的系统用,以继续为用户服务。一般用户信息的存储和用户登录认证都是在provider方完成。
client为平台系统,如:各个科技公司开发的系统,平台系统为用户提供更为具体的服务,比如:看新闻、看视频、玩游戏等等。client想要使用provider提供的第三方登录服务,必须要先向provider注册自己,也就是需要让provider识别自己是不是可信的平台服务商,所以在注册时provider会下发给各个client一组账号密码来唯一标识各个平台,从而可配置provider开放给各个client的访问域。
user即普通用户,普通用户使用平台商提供的服务。
传统的平台系统都各自有一套自己的用户认证体系,这就导致用户每使用一个平台都需要注册一组账号密码,时间久了就记不清了,从而导致用户的流失。使用OAuth 2.0提供的统一认证服务会很好的解决这个问题,用户只要记住一组账号密码,就可以在很多平台登录,提升了用户体验。
在oauth2-provider模块中,存放着用户的详细信息,一般包括用户名、密码、昵称、手机号、地址等等信息。需要在MyUserDetailsService的UserService实现自定义的用户认证方法,即:自定义实现用户表user,查询用户数据需要自己实现,同时实现MyUserDetails配置用户的账号密码、权限、是否过期、是否禁用等等,以便OAuth 2.0框架识别用户的一些基本信息以达到登录认证的作用。MyClientDetailsService的ClientService实现平台商的认证方法,对应到数据库的client表,存放平台商的一些基本账户信息和授权码回调地址,以及token过期时间。MyClientDetails配置了平台商的访问域、认证方式(主要有:authorization_code、password、client_credentials、implicit、refresh_ token五种,本文只介绍authorization_code方式的认证)、token过期等,系统根据你的配置就会限制平台商的一些访问操作及token过期等。
SecurityConfig中主要是开启spring-security和配置一些拦截过滤以及启用自定义用户认证,用户使用第三方登录时输入账号密码将在此处配置认证。ResourceServerConfig中主要配置对哪些资源路径进行拦截认证,例如:对获取用户的手机号拦截,对获取用户的昵称不进行拦截。AuthorizationServerConfig主要启用平台商认证配置。
在UserController中实现用户登录并授权成功后,client端可以通过下发的token访问到用户信息。
client端主要负责以下几个工作:
client端需要对用户暴露第三方登录入口,如:QQ登录、微信登录、支付宝登录等第三方授权平台。当用户选择第三方登录时,需要根据选择的入口提供不同的认证服务器登录页面的重定向。
当将用户重定向到第三方服务器进行登录并授权后,第三方服务器会根据平台商注册的回调地址将授权码回调给平台商。平台商根据授权码和平台商账号密码向第三方资源服务器获取access_token和refresh_token,使用access_token来获取用户信息,使用refresh_token来刷新access_token。
当平台商获取到用户信息后,会将用户信息保存到自身系统的用户表中,同时提示用户登录成功。第三方返回给平台商的用户信息可能会缺少必要信息,比如:手机号、住址等,第三方一般不会提供如此敏感的信息,这就需要平台商自己提示用户:绑定手机号享XXX服务等来引导用户继续完善自己的用户信息以获得更为精准的服务。
当用户使用第三方登录成功后,可能后续还会获取用户的其他资源,如:用户想在平台商提供的相册功能访问自己在第三方的相册,那么平台商就会继续使用access_token访问第三方服务器来获取用户的相册。此时可能access_token已经过期,那么client就需要在用户无感知的情况下使用refresh_token来换取新的access_token继续访问用户资源。众所周知,token都有过期时间,当refresh_token也过期的话,就需要告诉用户,登录已经过期,想要继续访问私有资源必要要重新登录,那么再进行登录流程获取用户信息和新的token。
项目采用多模块形式:
oauth2-parent.pom
4.0.0
com.xujianjie
oauth2-parent
1.0.0
pom
parent
oauth2-common
oauth2-client
oauth2-provider
UTF-8
1.8
1.8
org.springframework.boot
spring-boot-starter-parent
2.1.3.RELEASE
org.springframework.security.oauth.boot
spring-security-oauth2-autoconfigure
2.0.0.RELEASE
mysql
mysql-connector-java
5.1.25
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.0.0
com.alibaba
fastjson
1.2.54
org.json
json
20180813
oauth2-provider.pom
4.0.0
com.xujianjie
oauth2-parent
1.0.0
oauth2.provider
1.0.0
jar
oauth2-provider
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-starter-security
org.springframework.security.oauth.boot
spring-security-oauth2-autoconfigure
mysql
mysql-connector-java
org.mybatis.spring.boot
mybatis-spring-boot-starter
org.springframework.boot
spring-boot-maven-plugin
org.apache.maven.plugins
maven-surefire-plugin
2.20.1
true
oauth2-client.pom
4.0.0
com.xujianjie
oauth2-parent
1.0.0
oauth2.client
1.0.0
jar
oauth2-client
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
com.xujianjie
oauth2.common
1.0.0
mysql
mysql-connector-java
org.mybatis.spring.boot
mybatis-spring-boot-starter
com.alibaba
fastjson
org.json
json
org.springframework.boot
spring-boot-maven-plugin
org.apache.maven.plugins
maven-surefire-plugin
2.20.1
true
oauth2-common.pom
4.0.0
com.xujianjie
oauth2-parent
1.0.0
oauth2.common
1.0.0
jar
oauth2-common
org.springframework.boot
spring-boot-starter-web
SecurityConfig.java
package com.xujianjie.oauth2.provider.config;
import com.xujianjie.oauth2.provider.config.auth.service.MyUserDetailsService;
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.dao.DaoAuthenticationProvider;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
@Autowired
private MyUserDetailsService myUserDetailsService;
//密码加密
@Bean
public PasswordEncoder passwordEncoder()
{
return new BCryptPasswordEncoder();
}
//service用户认证
@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception
{
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(myUserDetailsService);
provider.setPasswordEncoder(passwordEncoder());
builder.authenticationProvider(provider);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception
{
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception
{
http.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/login")
.and()
.authorizeRequests()
.antMatchers("/login.html", "/img/**").permitAll()
.anyRequest().authenticated()
.and()
.csrf().disable();
}
}
ResourceServerConfig.java
package com.xujianjie.oauth2.provider.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
//资源服务器配置
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter
{
@Override
public void configure(ResourceServerSecurityConfigurer resources)
{
resources.resourceId("resource-info");
}
@Override
public void configure(HttpSecurity http) throws Exception
{
//配置拦截接口
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.requestMatchers()
.antMatchers("/api/user/**")
.and().authorizeRequests()
.antMatchers("/api/user/**")
.authenticated();
}
}
AuthorizationServerConfig.java
package com.xujianjie.oauth2.provider.config;
import com.xujianjie.oauth2.provider.config.auth.service.MyClientDetailsService;
import com.xujianjie.oauth2.provider.config.auth.service.MyUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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;
//授权服务器配置
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter
{
@Autowired
private MyUserDetailsService myUserDetailsService;
@Autowired
private MyClientDetailsService myClientDetailsService;
@Autowired
@Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
//service企业认证
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception
{
clients.withClientDetails(myClientDetailsService);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception
{
endpoints.authenticationManager(authenticationManager).userDetailsService(myUserDetailsService);
}
}
MyUserDetailsService.java
package com.xujianjie.oauth2.provider.config.auth.service;
import com.xujianjie.oauth2.provider.config.auth.model.MyUserDetails;
import com.xujianjie.oauth2.provider.model.User;
import com.xujianjie.oauth2.provider.service.UserService;
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.stereotype.Service;
@Service
public class MyUserDetailsService implements UserDetailsService
{
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username)
{
User user = userService.findByAccount(username);
if (user == null)
{
throw new UsernameNotFoundException(username);
}
return new MyUserDetails(user);
}
}
MyUserDetails.java
package com.xujianjie.oauth2.provider.config.auth.model;
import com.xujianjie.oauth2.provider.model.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
public class MyUserDetails implements UserDetails
{
private User user;
public MyUserDetails(User user)
{
this.user = user;
}
//权限列表
@Override
public Collection extends GrantedAuthority> getAuthorities()
{
return new ArrayList<>();
}
@Override
public String getPassword()
{
return user.getPassword();
}
@Override
public String getUsername()
{
return user.getAccount();
}
@Override
public boolean isAccountNonExpired()
{
return true;
}
@Override
public boolean isAccountNonLocked()
{
return true;
}
@Override
public boolean isCredentialsNonExpired()
{
return true;
}
@Override
public boolean isEnabled()
{
return true;
}
}
MyClientDetailsService.java
package com.xujianjie.oauth2.provider.config.auth.service;
import com.xujianjie.oauth2.provider.config.auth.model.MyClientDetails;
import com.xujianjie.oauth2.provider.model.Client;
import com.xujianjie.oauth2.provider.service.ClientService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.ClientRegistrationException;
import org.springframework.stereotype.Service;
@Service
public class MyClientDetailsService implements ClientDetailsService
{
@Autowired
private ClientService clientService;
@Override
public ClientDetails loadClientByClientId(String account) throws ClientRegistrationException
{
Client client = clientService.findByAccount(account);
if (client == null)
{
throw new ClientRegistrationException("企业客户未注册!");
}
return new MyClientDetails(client);
}
}
MyClientDetails.java
package com.xujianjie.oauth2.provider.config.auth.model;
import com.xujianjie.oauth2.provider.model.Client;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.provider.ClientDetails;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class MyClientDetails implements ClientDetails
{
private Client client;
public MyClientDetails(Client client)
{
this.client = client;
}
@Override
public String getClientId()
{
return client.getAccount();
}
@Override
public Set getResourceIds()
{
return new HashSet<>();
}
@Override
public boolean isSecretRequired()
{
return true;
}
@Override
public String getClientSecret()
{
return new BCryptPasswordEncoder().encode(client.getPassword());
}
@Override
public boolean isScoped()
{
return true;
}
@Override
public Set getScope()
{
Set set = new HashSet<>();
set.add("read");
return set;
}
@Override
public Set getAuthorizedGrantTypes()
{
Set set = new HashSet<>();
set.add("authorization_code");
set.add("refresh_token");
return set;
}
@Override
public Set getRegisteredRedirectUri()
{
Set set = new HashSet<>();
set.add(client.getCallBackUrl());
return set;
}
@Override
public Collection getAuthorities()
{
return new HashSet<>();
}
@Override
public Integer getAccessTokenValiditySeconds()
{
return client.getAccessTokenOverdueSeconds();
}
@Override
public Integer getRefreshTokenValiditySeconds()
{
return client.getRefreshTokenOverdueSeconds();
}
@Override
public boolean isAutoApprove(String s)
{
return true;
}
@Override
public Map getAdditionalInformation()
{
return new HashMap<>();
}
}
UserController.java
package com.xujianjie.oauth2.provider.controller;
import com.xujianjie.oauth2.provider.model.User;
import com.xujianjie.oauth2.provider.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.security.Principal;
@RestController
@RequestMapping("/api/user")
public class UserController
{
@Autowired
private UserService userService;
@RequestMapping(value = "/info", method = RequestMethod.GET)
public User info(Principal principal)
{
User user = userService.findByAccount(principal.getName());
user.setPassword("");
return user;
}
}
login.html
欢迎登录
application.properties
#端口号
server.port=8001
#mySql
spring.datasource.url=jdbc:mysql://localhost:3306/oauth2-provider
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#myBatis
#sql输出
logging.level.com.xujianjie.orgpermission.mapper=debug
#实体映射遵循驼峰命名
mybatis.configuration.mapUnderscoreToCamelCase=true
mybatis.configuration.useColumnLabel=true
UserController.java
package com.xujianjie.oauth2.client.controller;
import com.xujianjie.oauth2.client.exception.MyException;
import com.xujianjie.oauth2.client.model.params.UserLoginParams;
import com.xujianjie.oauth2.client.model.po.User;
import com.xujianjie.oauth2.client.service.UserService;
import com.xujianjie.oauth2.common.model.ResponseData;
import com.xujianjie.oauth2.common.utils.ServerUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import javax.servlet.http.HttpServletResponse;
@RestController
@RequestMapping(value = "/api/user")
public class UserController
{
@Autowired
private UserService userService;
@Value("${third-login-redirect-url}")
private String thirdLoginRedirectUrl;
@RequestMapping(value = "/thirdLogin", method = RequestMethod.GET)
public void thirdLogin(HttpServletResponse response)
{
try
{
//重定向到资源服务器进行身份验证
response.sendRedirect(thirdLoginRedirectUrl);
}
catch (IOException e)
{
e.printStackTrace();
}
}
@RequestMapping(value = "/thirdInfo")
public ResponseData thirdInfo(Integer userId)
{
if (!ServerUtil.validateObjectParamsSuccess(userId))
{
throw new MyException(ResponseData.STATUS_REQUEST_FAILED, "用户id不能为空!");
}
User user = userService.findThirdUser(userId);
if (user != null)
{
return new ResponseData(ResponseData.STATUS_OK, user, "获取成功!");
}
else
{
return new ResponseData(ResponseData.STATUS_REQUEST_FAILED, null, "用户不存在!");
}
}
}
TokenController.java
package com.xujianjie.oauth2.client.controller;
import com.xujianjie.oauth2.client.exception.MyException;
import com.xujianjie.oauth2.client.model.po.User;
import com.xujianjie.oauth2.client.service.UserService;
import com.xujianjie.oauth2.common.model.ResponseData;
import org.apache.tomcat.util.codec.binary.Base64;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestClientResponseException;
import org.springframework.web.client.RestTemplate;
@RestController
@RequestMapping("/token")
public class TokenController
{
@Value("${auth-call-back-url}")
private String authCallBackUrl;
@Value("${auth.client-id}")
private String clientId;
@Value("${auth.password}")
private String clientPassword;
@Value("${third-token-url}")
private String thirdTokenUrl;
@Value("${get-third-user-info-url}")
private String getThirdUserInfoUrl;
@Autowired
private UserService userService;
//第三方资源服务器返回用户认证成功标识authCode
@RequestMapping(value = "/callBack", method = RequestMethod.GET)
public ResponseData callBack(String code)
{
HttpHeaders headers = new HttpHeaders();
headers.add("authorization", "Basic " + new String(Base64.encodeBase64((clientId + ":" + clientPassword).getBytes())));
MultiValueMap params = new LinkedMultiValueMap<>();
params.add("grant_type", "authorization_code");
params.add("code", code);
params.add("client_id", clientId);
params.add("redirect_uri", authCallBackUrl);
try
{
String result = new RestTemplate().postForObject(thirdTokenUrl, new HttpEntity<>(params, headers), String.class);
if (result == null)
{
throw new MyException(ResponseData.STATUS_REQUEST_FAILED, "企业认证失败!");
}
JSONObject json = new JSONObject(result);
return handleThirdUser(json.optString("access_token"), json.optString("refresh_token"));
}
catch (RestClientResponseException e)
{
e.printStackTrace();
}
return new ResponseData(ResponseData.STATUS_REQUEST_FAILED, "", "登录失败!");
}
private ResponseData handleThirdUser(String accessToken, String refreshToken)
{
HttpHeaders headers = new HttpHeaders();
headers.add("authorization", "Bearer " + accessToken);
try
{
String result = new RestTemplate().exchange(getThirdUserInfoUrl, HttpMethod.GET, new HttpEntity<>(headers), String.class).getBody();
//将第三方用户写入本系统中
JSONObject json = new JSONObject(result);
User user = new User();
user.setAccount("user_" + System.currentTimeMillis());
user.setPassword("123456");
user.setNickName(json.optString("nickName"));
user.setMobile(json.optString("mobile"));
user.setThirdAccount(json.optString("account"));
user.setAccessToken(accessToken);
user.setRefreshToken(refreshToken);
return new ResponseData(ResponseData.STATUS_OK, userService.addUser(user), "登录成功!");
}
catch (RestClientResponseException e)
{
e.printStackTrace();
}
return new ResponseData(ResponseData.STATUS_REQUEST_FAILED, "", "登录失败!");
}
}
UserServiceImpl.java
package com.xujianjie.oauth2.client.service.impl;
import com.xujianjie.oauth2.client.exception.MyException;
import com.xujianjie.oauth2.client.mapper.UserMapper;
import com.xujianjie.oauth2.client.model.po.User;
import com.xujianjie.oauth2.client.service.UserService;
import com.xujianjie.oauth2.common.model.ResponseData;
import org.apache.tomcat.util.codec.binary.Base64;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClientResponseException;
import org.springframework.web.client.RestTemplate;
@Transactional
@Service
public class UserServiceImpl implements UserService
{
@Value("${get-third-user-info-url}")
private String getThirdUserInfoUrl;
@Value("${third-token-url}")
private String thirdTokenUrl;
@Value("${auth.client-id}")
private String clientId;
@Value("${auth.password}")
private String clientPassword;
@Autowired
private UserMapper userMapper;
@Override
public User addUser(User user)
{
String thirdAccount = user.getThirdAccount();
if (thirdAccount != null && !thirdAccount.equals(""))
{
User findUser = userMapper.findByThirdAccount(thirdAccount);
if (findUser == null)
{
if (userMapper.insert(user) == 1)
{
return user;
}
}
else
{
user.setUserId(findUser.getUserId());
return updateUser(user);
}
}
return null;
}
@Override
public User findById(int userId)
{
return userMapper.findById(userId);
}
@Override
public User updateUser(User user)
{
User updateUser = findById(user.getUserId());
if (updateUser == null)
{
throw new MyException(ResponseData.STATUS_REQUEST_FAILED, "用户不存在!");
}
if (user.getPassword() != null)
{
updateUser.setPassword(user.getPassword());
}
if (user.getNickName() != null)
{
updateUser.setNickName(user.getNickName());
}
if (user.getMobile() != null)
{
updateUser.setMobile(user.getMobile());
}
if (user.getAccessToken() != null)
{
updateUser.setAccessToken(user.getAccessToken());
}
if (user.getRefreshToken() != null)
{
updateUser.setRefreshToken(user.getRefreshToken());
}
if (userMapper.update(updateUser) == 1)
{
return updateUser;
}
else
{
throw new MyException(ResponseData.STATUS_REQUEST_FAILED, "更新失败!");
}
}
@Override
public User validateUser(String account, String password)
{
User findUser = userMapper.findByAccount(account);
if (findUser != null && findUser.getPassword().equals(password))
{
return findUser;
}
return null;
}
@Override
public User findThirdUser(int userId)
{
User findUser = findById(userId);
String accessToken = findUser.getAccessToken();
if (accessToken != null && !accessToken.equals(""))
{
HttpHeaders headers = new HttpHeaders();
headers.add("authorization", "Bearer " + accessToken);
try
{
String result = new RestTemplate().exchange(getThirdUserInfoUrl, HttpMethod.GET, new HttpEntity<>(headers), String.class).getBody();
//更新用户信息
JSONObject json = new JSONObject(result);
findUser.setNickName(json.optString("nickName"));
findUser.setMobile(json.optString("mobile"));
findUser.setThirdAccount(json.optString("account"));
updateUser(findUser);
return findUser;
}
catch (RestClientResponseException e)
{
if (e.getRawStatusCode() == 401)
{
String newAccessToken = refreshAccessToken(findUser.getRefreshToken());
if (newAccessToken.equals(""))
{
//refreshToken过期后清空refreshToken和aceessToken
findUser.setAccessToken("");
findUser.setRefreshToken("");
updateUser(findUser);
throw new MyException(ResponseData.STATUS_IDENTITY_OVERDUE, "第三方登录已过期,请重新登录!");
}
//登录未过期,更新accessToken
findUser.setAccessToken(newAccessToken);
updateUser(findUser);
return findThirdUser(userId);
}
}
}
return null;
}
//以refreshToken刷新accessToken
private String refreshAccessToken(String refreshToken)
{
HttpHeaders headers = new HttpHeaders();
headers.add("authorization", "Basic " + new String(Base64.encodeBase64((clientId + ":" + clientPassword).getBytes())));
MultiValueMap params = new LinkedMultiValueMap<>();
params.add("grant_type", "refresh_token");
params.add("refresh_token", refreshToken);
String accessToken;
try
{
String result = new RestTemplate().postForObject(thirdTokenUrl, new HttpEntity<>(params, headers), String.class);
accessToken = new JSONObject(result).optString("access_token");
}
catch (RestClientResponseException e)
{
//400 badRequest
accessToken = "";
}
return accessToken;
}
}
application.properties
#ip地址
ip-address: localhost
#端口号
server.port=8002
#mySql
spring.datasource.url=jdbc:mysql://localhost:3306/oauth2-client
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#myBatis
#sql输出
logging.level.com.xujianjie.orgpermission.mapper=debug
#实体映射遵循驼峰命名
mybatis.configuration.mapUnderscoreToCamelCase=true
mybatis.configuration.useColumnLabel=true
#客户端账号
auth.client-id=yncw
auth.password=123456
#认证重定向地址
third-login-redirect-url=http://localhost:8001/oauth/authorize?response_type=code&client_id=${auth.client-id}&redirect_uri=${auth-call-back-url}
#authCode回调地址
auth-call-back-url=http://${ip-address}:${server.port}/token/callBack
#从资源服务器获取和刷新token地址
third-token-url=http://localhost:8001/oauth/token
#登录成功后获取用户信息
get-third-user-info-url=http://localhost:8001/api/user/info
MYSQL数据库:
oauth2-provider.sql
/*
Navicat MySQL Data Transfer
Source Server : localhost
Source Server Version : 50721
Source Host : localhost:3306
Source Database : oauth2-provider
Target Server Type : MYSQL
Target Server Version : 50721
File Encoding : 65001
Date: 2019-08-20 11:56:47
*/
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for client
-- ----------------------------
DROP TABLE IF EXISTS `client`;
CREATE TABLE `client` (
`client_id` int(1) NOT NULL AUTO_INCREMENT COMMENT '客户企业id',
`client_name` varchar(100) DEFAULT NULL COMMENT '企业名称',
`account` varchar(100) DEFAULT NULL COMMENT '企业账号',
`password` varchar(100) DEFAULT NULL COMMENT '密码',
`call_back_url` varchar(255) DEFAULT NULL COMMENT '认证回调地址',
`access_token_overdue_seconds` int(1) DEFAULT NULL COMMENT 'access_token过期时间(秒)',
`refresh_token_overdue_seconds` int(1) DEFAULT NULL COMMENT 'refresh_token过期时间(秒)',
PRIMARY KEY (`client_id`),
UNIQUE KEY `index_account` (`account`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of client
-- ----------------------------
INSERT INTO `client` VALUES ('1', '云南成为智能科技有限公司', 'yncw', '123456', 'http://localhost:8002/token/callBack', '20', '60');
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`user_id` int(1) NOT NULL AUTO_INCREMENT COMMENT '用户id',
`account` varchar(100) DEFAULT NULL COMMENT '用户名',
`password` varchar(100) DEFAULT NULL COMMENT '密码',
`nick_name` varchar(100) DEFAULT NULL COMMENT '用户昵称',
`head_icon` varchar(255) DEFAULT NULL COMMENT '用户头像地址',
`mobile` varchar(100) DEFAULT NULL COMMENT '手机号',
PRIMARY KEY (`user_id`),
UNIQUE KEY `index_account` (`account`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', 'xujianjie', '$2a$10$.5xnLBrwhj/qjdj/W8tYde.K8ALuC1wbUg8YYFMpx0jjeB7tM3Ctu', '徐建杰', '/head-icon.png', '18887654321');
oauth2-client.sql
/*
Navicat MySQL Data Transfer
Source Server : localhost
Source Server Version : 50721
Source Host : localhost:3306
Source Database : oauth2-client
Target Server Type : MYSQL
Target Server Version : 50721
File Encoding : 65001
Date: 2019-08-20 11:56:39
*/
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`user_id` int(1) NOT NULL AUTO_INCREMENT COMMENT '用户id',
`account` varchar(100) DEFAULT NULL COMMENT '用户名',
`password` varchar(100) DEFAULT NULL COMMENT '密码',
`nick_name` varchar(100) DEFAULT NULL COMMENT '用户昵称',
`mobile` varchar(100) DEFAULT NULL COMMENT '手机号',
`third_account` varchar(100) DEFAULT NULL COMMENT '第三方用户名',
`access_token` varchar(255) DEFAULT NULL COMMENT '第三方服务器token',
`refresh_token` varchar(255) DEFAULT NULL COMMENT '第三方服务器刷新access_token的token',
PRIMARY KEY (`user_id`),
UNIQUE KEY `index_account` (`account`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', 'admin', '123456', '超级管理员', '18888888888', null, null, null);
INSERT INTO `user` VALUES ('4', 'user_1566011157468', '123456', '徐建杰', '18887654321', 'xujianjie', '091d473f-64de-46da-9942-cc61bd094892', '889dc870-bfec-407c-b3bc-4bb8d6e83fe2');
打开浏览器,输入地址:http://localhost:8002/api/user/thirdLogin 会自动重定向到8001服务器进行用户认证:
输入用户名密码认证通过后,client端会自动为用户进行token获取与刷新、用户信息获取与存储等操作,所展现给用户的操作就是:选择第三方登录、进行第三方登录、第三方登录成功。client端会处理与provider端的复杂交互。
获取用户信息:
access_token过期后client端会自动刷新token,若是refresh_token也过期的话,会提示用户重新登录,此时需要再次访问http://localhost:8002/api/user/thirdLogin 登录后就可以让client端获取新的token继续访问用户信息。
将近两个星期的OAuth 2.0的研究,基本掌握如何使用这套框架进行用户认证与提供单点登录服务,也搜索了大量文章,但是很多文章没有系统详细的说明,所以今天把它记录在这,希望能帮助更多想使用OAuth 2.0的开发者!
谢谢查阅!