Using springboot environment of java back-end to realize QQ third-party login for website access

Explain

Spring boot environment based on the introduction of Spring MVC.

Access the official documents of QQ: Portal

Access qualifications to obtain the website's app_id and app_key and other content official network has been enough detail, this will not be repeated here. Every step to QQ to provide which API web site to send requests, what parameters to bring and other official documents have also been introduced clearly, no longer to elaborate.

The key point is how to write the java code, why and what pits I encounter in the process of complete login by calling the third party login API of QQ with java back-end technology, and getting the openid that can uniquely identify a QQ.

Under the class resource folder resources, a QQLogin.properties file is stored, which stores the parameters that need to be used in the whole process. The subsequent QQLoginUtil. getQLoginInfo (...) reads the corresponding content from this file. Specific implementation is not the focus of this article, you can refer to this article: Several methods of reading. properties configuration file in java.

Official Document Section 1: Preparations _OAuth 2.0

The documentation is sufficiently detailed. But emphasize that the callback address in the application settings must be exactly the same as the callback address we used later. Don't think that it's not allowed for me to fill in a / Handler in the callback address and write / Handler/AHandler in the callback address.

Section 2 of the Official Document: Place the "QQ Login" button _OAuth2.0

Most of the front-end content, this paper mainly discusses the back-end code, so do not discuss more. But here we will click on the QQ login button hyperlink location to send a request to our own back-end server, because the whole process is completed by our back-end.

Here's the structure of our back end.

@Controller
@RequestMapping("/login")
public class loginServlet {
    @RequestMapping("/loginByQQ")
    public void loginByQQ(HttpServletRequest req, HttpServletResponse resp) throws Exception {}

    @RequestMapping("/loginByQQCallbackHandler")
    public void loginByQQCallbackHandler(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {}
}

So I located the hyperlink of QQ login to / login/loginByQQ.

Section 3 of the Official Document: Getting Access_Token with Authorization_Code

Step1: Get Authorization Code

There is nothing to say about this step. Official documents make it clear to whom to send requests, what parameters to take and how to respond to these three points after they are sent out. So the first step in the code is to get the value of the parameter from the configuration file, and then form the URL to be sent through the format method of String. It is also possible to use + for string stitching many times here, but this is more semantically understandable. The server then jumps to the specified URL.

    @RequestMapping("/loginByQQ")
    public void loginByQQ(HttpServletRequest req, HttpServletResponse resp) throws Exception {

        String response_type = QQLoginUtil.getQQLoginInfo("response_type");
        String client_id = QQLoginUtil.getQQLoginInfo("client_id");
        String redirect_uri = QQLoginUtil.getQQLoginInfo("redirect_uri");
        //State value of client end. For third-party applications to prevent CSRF attacks.
        String state = new Date().toString();
        req.getSession().setAttribute("state", state);

        String url = String.format("https://graph.qq.com/oauth2.0/authorize" +
                "?response_type=%s&client_id=%s&redirect_uri=%s&state=%s", response_type, client_id, redirect_uri, state);
                
        resp.sendRedirect(url);
    }

According to the controller path shown in the second section of this article, the callback path must end with / login/loginByQQCallbackHandler, so that when this step makes a request, the QQ side with the said parameters (in the form of request parameters of get) jumps back, it will be handled by the loginByQCallbackHandler method.

Step2: Get Access Token through Authorization Code

First, get Authorization Code: String authorization_code = req.getParameter("code");

Then we complete our URL as required by the official document:`

if (authorization_code != null && !authorization_code.trim().isEmpty()) {
    //State value of client end. For third-party applications to prevent CSRF attacks.
    String state = req.getParameter("state");
    if (!state.equals(req.getParameter("state"))) {
        throw new RuntimeException("client The state value of the end does not match!");
    }
    String urlForAccessToken = getUrlForAccessToken(authorization_code);
}

We first verify the state parameters mentioned in the previous official documents for pre-and post-verification. Only by passing can we allow the next step.

Then there is the implementation of getUrlForAccessToken:

    public String getUrlForAccessToken(String authorization_code) {
        String grant_type = QQLoginUtil.getQQLoginInfo("grant_type");
        String client_id = QQLoginUtil.getQQLoginInfo("client_id");
        String client_secret = QQLoginUtil.getQQLoginInfo("client_secret");
        String redirect_uri = QQLoginUtil.getQQLoginInfo("redirect_uri");
        
        String url = String.format("https://graph.qq.com/oauth2.0/token" +
                        "?grant_type=%s&client_id=%s&client_secret=%s&code=%s&redirect_uri=%s",
                grant_type, client_id, client_secret, authorization_code, redirect_uri);
        
        return url;
    }

There's nothing to say about all this.

Then we jump the URL to get access_token. This is the first pit. According to the official documents, it seems that when we jump to this URL to get access_token, Tencent will jump the callback address we set and take the parameters we need, just like before we got authorization code. . But that's not the case at all!!! When you send a request to the URL that gets access_token as required, the other party will not jump back, but will return a data directly to you. I hope you can get the data and process it. This is a bit like the callback function that handles data after the asynchronous request of the front-end JS.

So here we also use the java backend to simulate the client to initiate the request, so I use the RestTemplate module in the Spring container, which is coded as follows:

RestTemplate restTemplate = (RestTemplate) applicationContext.getBean("RestTemplate");

You can see that we registered the RestTemplate object in the spring container. The registration code is in the root configuration file of spring boot. The specific code is as follows:

    @Bean(name="RestTemplate")
    @Autowired
    public RestTemplate getRestTemplate(RestTemplateBuilder restTemplateBuilder){
        RestTemplate restTemplate = restTemplateBuilder.build();
        return restTemplate;
    }

RestTemplateBuilder class object because RestTemplate is a Spring MVC integrated module, it has built-in configuration and enough for us to use, just use the @Autowire annotation injection.

As for how to get the Spring container in the LoginServlet Controller: After defining a member variable of the ApplicationContext class in the loginServlet class and using the @Autowire annotation, Spring automatically injects the container into it. As to whether this is more elegant to achieve, the answer seems to be no at present: Is there any elegant way Springboot can call get Bean (String beanname) to get beans?

Now that we have figured out how to get an instance of RestTemplate and the URL used to get access token, we use restTemplate to initiate GET requests and process them.

//The first time to initiate simulated client access with a server, you get a string containing access_token in the following format
//access_token=0FFD92ABD1DFD4F5&expires_in=7776000&refresh_token=04CE5D1F1E290B0974C5
String firstCallbackInfo = restTemplate.getForObject(urlForAccessToken, String.class);
String[] params = firstCallbackInfo.split("&");
String access_token = null;
for (String param : params) {
    String[] keyvalue = param.split("=");
    if(keyvalue[0].equals("access_token")){
        access_token = keyvalue[1];
        break;
    }
}

Yes, as the official document says, the data you get back is access_token=0FFD92ABD1DFD4F5&expires_in=7776000&refresh_token=04CE5D1F1E290B0974C5. It's not a JSON. Tencent has really led the industry in API for a hundred years. This may be the best format for human beings to recover from the Fourth World War to the electrical age after 500 years.
The way I deal with it here is to divide it into three segments and then divide each segment into two strings with = so that we can think of the two strings as key-value relations, and then get the value of key=access_token, which is our goal.

Also mention that the getForObject method of RestTemplate class supports JSON very well. You can fill in the bytecode of the entity class corresponding to JSON in the second parameter, but if you don't bother RestTemplate (which is actually parsed by Spring boot configuration Jackson), you can pass String.class to the original. The return data of this book.

Step3: (optional) Automated renewal of permissions to obtain Access Token

I didn't use it.

Section 4 of the Official Document: Getting User OpenID_OAuth 2.0

The technical points of data acquisition are mentioned in the previous section, but the only difficulty is that the data returned this time is more difficult to handle. Callback ({"client_id": "YOUR_APPID", "openid": "YOUR_OPENID"}); in this JSONP format, Tencent is really awesome.

Let's go ahead and get the code that returns the data.

if (access_token != null && !access_token.trim().isEmpty()) {
    String url = String.format("https://graph.qq.com/oauth2.0/me?access_token=%s", access_token);
    //After the second simulation client sends out the request, it gets the return data with openid in the following format
    //callback( {"client_id":"YOUR_APPID","openid":"YOUR_OPENID"} );
    String secondCallbackInfo = restTemplate.getForObject(url, String.class);
}

Next, how do we deal with this data? My idea is to use regular expressions (regular expression functions integrated into JDK), intercept {"client_id", "YOUR_APPID", "openid", "YOUR_OPENID"} and convert it into a Map pair using the jackson module integrated inside spring boot. After the image is obtained by get method, the code is as follows

//Regular expression processing
String regex = "\\{.*\\}";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(secondCallbackInfo);
if(!matcher.find()){
    throw new RuntimeException("Abnormal callback value: "+secondCallbackInfo);
}

//Call jackson
ObjectMapper objectMapper = new ObjectMapper();
HashMap hashMap = objectMapper.readValue(matcher.group(0), HashMap.class);

String openid = ((String) hashMap.get("openid"));

Here are two points worth mentioning.

Firstly, why does String regex = "\{*\}"; have \ in regular expressions? Because {and} in regular expressions are meaningful and non-character, we want regular expressions to understand them as characters, so we need to escape them. So we need an escape character here, but\ itself is not a character in the java string, so we also need to escape itself, so there will be\.

Secondly, if matcher does not experience matcher.find(), then even if there is a suitable matching content, there will still be no matching can be obtained. So matcher.find() is necessary, and matcher.find() comes again after one time. That's all. Return false.

So far, we have completed the opening Id, which can only identify a QQ, and then the database and other operations are not our focus. As for further acquisition of user nicknames, avatars, combined with official documents and the experience gained from the above experience, I don't think there will be any major problems. So that's the end of this article.

Some other things learned

Because I want to read the properties file under the class resource path, and I don't want to load the unrelated code of the Properties object into the method, I set up a tool class to centralize the process. But there's a problem: you can't use this.class.getClassLoader().getResourceAsStream(...) under the static method because this doesn't come out. Then change it to the name of the class: QQLoginUtil.class.getClassLoader().getResourceAsStream(...)

Keywords: Java Spring JSON network

Added by phynias on Sun, 25 Aug 2019 18:13:48 +0300