preface
This article will introduce how to access the GitHub user information API based on OAuth2 protocol and how to implement a simple authentication server based on the authorization code mode. If you are not familiar with the basic concepts of OAuth2 and the four authorization modes, you can first read ruan Yifong’s blog: A simple explanation of OAuth 2.0, this article is mainly to explain how to use the actual demo. The complete code for the example shown in this article has been uploaded to GitHub.
GitHub third-party login
Lead to
Before access lot API interface, need to visit https://github.com/settings/applications/new, then fill in the following content:
Except for the last item, the Authorization callback URL, which is used to display the website information when the user clicks on a third party to log in, and the last item is the callback address used to receive the temporary Authorization code in exchange for Access Token. The corresponding figure of D and E, below from https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.
+----------+ | Resource | | Owner | | | +----------+ ^ | (B) +----|-----+ Client Identifier +---------------+ | -+----(A)-- & Redirection URI ---->| | | User- | | Authorization | | Agent -+----(B)-- User authenticates --->| Server | | | | | | -+----(C)-- Authorization Code ---<| | +-|----|---+ +---------------+ | | ^ v (A) (C) | | | | | | ^ v | | +---------+ | | | |>---(D)-- Authorization Code ---------' | | Client | & Redirection URI | | | | | |<---(E)----- Access Token -------------------' +---------+ (w/ Optional Refresh Token) Note: The lines illustrating steps (A), (B), and (C) are broken into two parts as they pass through the user-agent. Authorization Code FlowCopy the code
After filling in the above information, it will jump to the following interface:
Here you need to save the Client ID and Client secret generated after clicking Generate a new Client secret for future use.
coding
After completing the above preparation steps, the coding work can start. First of all, in order to facilitate subsequent use and modification, Client ID and Client Secret can be configured in the configuration file (to reduce the length, only part of the core code is shown) :
server:
port: 8080
oauth:
github:
# Replace with your own Client ID and Client Secret
clientId: 8aed0bc8316548e9d26d
clientSecret: fdd0e7af5052164e459098703005c5db25f857a8
GitHub user information generated after the token is passed to the front-end processing address
frontRedirectUrl: http://localhost/redirect
# a HTTP client (https://github.com/LianjiaTech/retrofit-spring-boot-starter) framework configuration
retrofit:
global-connect-timeout-ms: 20000
global-read-timeout-ms: 10000
Copy the code
Then write the corresponding entity GithubAuth:
/** * Github authentication information **@author zjw
* @dateThe 2021-10-23 * /
@Data
@Component
@ConfigurationProperties(prefix = "oauth.github")
public class GithubAuth {
/** * Client id */
private String clientId;
/** * Client key */
private String clientSecret;
/** * Redirect address */
private String frontRedirectUrl;
}
Copy the code
Then write the GitHub authentication interface service class:
/** * Github oauth interface service class **@author zjw
* @dateThe 2021-10-23 * /
@RetrofitClient(baseUrl = "https://github.com/login/oauth/")
public interface GithubAuthService {
/** * Make a Github authorization request **@paramClientId clientId *@paramClientSecret Client key *@paramCode Temporary authorization code *@return access_token
*/
@POST("access_token")
@Headers("Accept: application/json")
GithubToken getToken(
@Query("client_id") String clientId,
@Query("client_secret") String clientSecret,
@Query("code") String code);
}
Copy the code
And interface service classes to get user information:
/** * Github interface service class **@author zjw
* @dateThe 2021-10-23 * /
@RetrofitClient(baseUrl = "https://api.github.com")
public interface GithubApiService {
/** * Get github user information based on access_token **@paramAuthorization Request authentication header *@returnMaking the user * /
@GET("/user")
GithubUser getUserInfo(@Header(HttpHeaders.AUTHORIZATION) String authorization);
}
Copy the code
Then the interface that handles the temporary authorization code (where the interface address corresponds to the callback address filled in above) :
/** * oauth2 Authenticate controller **@author zjw
* @dateThe 2021-10-23 * /
@RestController
@RequestMapping("/oauth")
public class OauthController {
@Resource
private GithubAuth githubAuth;
@Resource
private GithubApiService githubApiService;
@Resource
private GithubAuthService githubAuthService;
/** * github redirect address **@paramCode Temporary authorization code *@paramThe response response * /
@GetMapping("/github/redirect")
public void githubRedirect(String code, HttpServletResponse response) {
/ / get access_token
String clientId = githubAuth.getClientId();
String clientSecret = githubAuth.getClientSecret();
GithubToken githubToken = githubAuthService.getToken(clientId, clientSecret, code);
// Get github user information
String authorization = String.join(
StringUtils.SPACE, githubToken.getTokenType(), githubToken.getAccessToken());
GithubUser githubUser = githubApiService.getUserInfo(authorization);
// Generate a local access token
String token = JwtUtils.sign(githubUser.getUsername(), UserType.GITHUB.getType());
try {
response.sendRedirect(githubAuth.getFrontRedirectUrl() + "? token=" + token);
} catch (IOException e) {
throw newApiException(REDIRECT_FAILED); }}}Copy the code
The front end only needs to place the corresponding GitHub icon on the login page and set the click event:
githubAuthorize() {
const env = process.env
window.location.href = `https://github.com/login/oauth/authorize?
client_id=${env.VUE_APP_GITHUB_CLIENT_ID}
&redirect_uri=${env.VUE_APP_GITHUB_REDIRECT_URI}`
}
Copy the code
And perform the following redirect processing on the redirection interface:
created() {
this.setToken(this.$route.query.token)
this.$router.push('/')}Copy the code
This is the core configuration of the project, and the final result is as follows:
Self-implementation of OAuth2 authentication server
Lead to
In this article, database storage is used to save client information. Therefore, the following SQL script needs to be executed first to create the corresponding table:
-- ----------------------------
-- Table structure for oauth_client_details
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
`client_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`resource_ids` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`client_secret` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`scope` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`authorized_grant_types` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`web_server_redirect_uri` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`authorities` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`access_token_validity` int(11) NULL DEFAULT NULL,
`refresh_token_validity` int(11) NULL DEFAULT NULL,
`additional_information` varchar(4096) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`autoapprove` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL.PRIMARY KEY (`client_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of oauth_client_details
-- ----------------------------
INSERT INTO `oauth_client_details` VALUES ('butterfly'.NULL.'$2a$10$KvJWyf4wI.YcpzmbYGw8NOSlauim7dF9b/VSMOomONJf40Bq8F4Me'.'all'.'authorization_code'.'http://localhost:8080/oauth/oauth2/redirect'.NULL.3600.7200.NULL.'false');
Copy the code
coding
The first is the YAML configuration:
server:
port: 9002
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
jdbcUrl: jdbc:mysql://localhost:3306/oauth2? useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: root
Copy the code
Next is the configuration of the authentication server:
/** * Authentication server configuration **@author zjw
* @dateThe 2021-10-13 * /
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Resource
private UserDetailsServiceImpl userDetailsService;
/** * Configure access_token */ of type JWT
@Bean
public TokenStore tokenStore(a) {
return new JwtTokenStore(jwtAccessTokenConverter());
}
/** * Set the token signature */
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(a) {
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
accessTokenConverter.setSigningKey(BaseConstants.SECRET);
return accessTokenConverter;
}
/** * Configure the authentication data source */
@Bean
@Primary
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource(a) {
return DataSourceBuilder.create().build();
}
/** * Configure the JDBC authentication mode */
@Bean
public ClientDetailsService jdbcClientDetails(a) {
return new JdbcClientDetailsService(dataSource());
}
/** * Configure the JDBC authentication mode */
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(jdbcClientDetails());
}
/** * Configure the information format and content of the certificate */
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.tokenStore(tokenStore())
.accessTokenConverter(jwtAccessTokenConverter())
// Obtain refresh_token
.userDetailsService(userDetailsService);
}
/** * Enable token authentication */
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security.allowFormAuthenticationForClients()
.tokenKeyAccess("isAuthenticated()")
.checkTokenAccess("isAuthenticated()"); }}Copy the code
Then there is the custom user permission information, where you can set the user and permission information stored in the token:
/** * Set user permission information **@author zjw
* @dateThe 2021-10-13 * /
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private IUserService userService;
/** * Sets the authentication information stored in access_token */
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.getByUsername(username);
if (user == null) {
throw new ApiException(BaseConstants.AUTHENTICATION_FAILED);
}
String account = user.getUsername();
return new org.springframework.security.core.userdetails.User(
account, user.getPassword(),
Collections.singletonList(newSimpleGrantedAuthority(account)) ); }}Copy the code
Then Web security-related configurations:
/** * Web security configuration **@author zjw
* @dateThe 2021-10-13 * /
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
/** * Password encryption mode */
@Bean
public BCryptPasswordEncoder passwordEncoder(a) {
return new BCryptPasswordEncoder();
}
/** * Custom user permission configuration */
@Bean
@Override
public UserDetailsService userDetailsService(a) {
return new UserDetailsServiceImpl();
}
/** * Sets the permission information stored in the token */
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService())
.passwordEncoder(passwordEncoder());
}
/** * Cross-domain configuration */
@Bean
public CorsConfigurationSource corsConfigurationSource(a) {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Collections.singletonList(ALL));
configuration.setAllowedMethods(Arrays.asList(
HttpMethod.POST.name(), HttpMethod.GET.name(), HttpMethod.OPTIONS.name()));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration(ALL_PATTERN, configuration);
returnsource; }}Copy the code
The above is all the configuration of the authorization server. The following shows the configuration of the resource server. Here, the authorization server and the resource server are still configured separately, and the old configuration method is still used:
/** * Resource server **@author zjw
* @dateThe 2021-11-21 * /
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/user/**").access("#oauth2.hasScope('all')"); }}Copy the code
Then create an interface that returns user information:
/** * Resource controller **@author zjw
* @dateThe 2021-11-20 * /
@RestController
public class ResourceController {
/** * Obtain the user name based on the token **@paramAuthorization Token Request header *@returnUser name * /
@GetMapping("/user/info")
public String info(@RequestHeader(HttpHeaders.AUTHORIZATION) String authorization) {
String token = authorization.substring(TokenConstants.BEARER_PREFIX.length());
returnJWT.decode(token).getClaim(TokenConstants.USERNAME).asString(); }}Copy the code
The yamL configuration is as follows, where http://localhost:9002 needs to be changed to the address of the authorization server:
server:
port: 9003
security:
oauth2:
client:
client-id: butterfly
client-secret: 123456
access-token-uri: http://localhost:9002/oauth/token
user-authorization-uri: http://localhost:9002/oauth/authorize
resource:
token-info-uri: http://localhost:9002/oauth/check_token
Copy the code
After completing the above configuration, you can follow the steps in GitHub to write the client back-end code:
The first is the YAML configuration:
server:
port: 8080
oauth:
oauth2:
clientId: butterfly
clientSecret: 123456
frontRedirectUrl: http://localhost/redirect
retrofit:
global-connect-timeout-ms: 20000
global-read-timeout-ms: 10000
Copy the code
Then create the corresponding entity:
/** * oauth2 Authentication information **@author zjw
* @dateThe 2021-10-31 * /
@Data
@Component
@ConfigurationProperties(prefix = "oauth.oauth2")
public class Oauth2Auth {
/** * Client id */
private String clientId;
/** * Client key */
private String clientSecret;
/** * Redirect address */
private String frontRedirectUrl;
}
Copy the code
Then write the corresponding authentication and access to user information interface service class:
/** * oauth2 interface service class **@author zjw
* @dateThe 2021-10-31 * /
@RetrofitClient(baseUrl = "http://localhost:9002/oauth/")
public interface Oauth2AuthService {
/** * Make oauth2 authorization request **@paramAuthorization header *@paramCode Temporary authorization code *@paramGrantType Authentication type *@return access_token
*/
@POST("token")
Oauth2Token getToken(
@Header(HttpHeaders.AUTHORIZATION) String authorization,
@Query("code") String code,
@Query("grant_type") String grantType);
}
Copy the code
/** * oauth2 interface service class **@author zjw
* @dateThe 2021-10-31 * /
@RetrofitClient(baseUrl = "http://localhost:9003")
public interface Oauth2ApiService {
/** * Get oauth2 user name ** based on access_token@paramAuthorization Request authentication header *@returnOauth2 User name */
@GET("/user/info")
String getUserInfo(@Header(HttpHeaders.AUTHORIZATION) String authorization);
}
Copy the code
Then write a callback interface that handles temporary authorization code:
/** * oauth2 Authenticate controller **@author zjw
* @dateThe 2021-10-23 * /
@RestController
@RequestMapping("/oauth")
public class OauthController {
@Resource
private Oauth2Auth oauth2Auth;
@Resource
private Oauth2AuthService oauth2AuthService;
@Resource
private Oauth2ApiService oauth2ApiService;
/** * oauth2 redirects address **@paramCode Temporary authorization code *@paramThe response response * /
@GetMapping("/oauth2/redirect")
public void oauth2Redirect(String code, HttpServletResponse response) {
/ / get access_token
String clientId = oauth2Auth.getClientId();
String clientSecret = oauth2Auth.getClientSecret();
String authorization = BaseConstants.BASIC_TYPE + Base64.getEncoder().encodeToString(
(String.join(":", clientId, clientSecret)).getBytes()
);
// Get the oauth2 user name
Oauth2Token oauth2Token = oauth2AuthService.getToken(
authorization, code, "authorization_code");
String username = oauth2ApiService.getUserInfo(
String.join(
StringUtils.SPACE,
oauth2Token.getTokenType(),
oauth2Token.getAccessToken()
)
);
// Generate a local access token
String token = JwtUtils.sign(username, UserType.OAUTH2.getType());
try {
response.sendRedirect(oauth2Auth.getFrontRedirectUrl() + "? token=" + token);
} catch (IOException e) {
throw newApiException(REDIRECT_FAILED); }}}Copy the code
Then add the corresponding icon click event to the front as well:
oauth2Authorize() {
const env = process.env
window.location.href = `http://localhost:9002/oauth/authorize?
client_id=${env.VUE_APP_OAUTH_CLIENT_ID}&response_type=code`
}
Copy the code
The redirect processing for the redirection interface remains unchanged:
created() {
this.setToken(this.$route.query.token)
this.$router.push('/')}Copy the code
The final effect is as follows:
conclusion
This article briefly explains how to access GitHub’s third-party login interface and implement a simple authentication server. In future articles, we will explain how to customize the login and confirmation interface of the authentication server and add custom authentication methods.