Spring SecuritySpring-Boot

Spring-Security-教程-(五)---实现QQ授权登

2019-04-03  本文已影响2人  老亚瑟程序猿

一、准备

1.1 先在QQ互联申请成为开发者,并创建应用申请APPId 和AppKey。
1.2 查看官方接入流程文档

以下是自己申请后的密钥,贡献出来方便各位测试

APP ID:101364240
APP Key:ef27b7a6ca651a3609dd47f21e385955
回调地址:http://127.0.0.1/login/qq

项目代码:https://github.com/Bootcap/spring-security-study-session

二、开发步骤

前提,在上篇Spring Security (四) - 权限动态修改

pom.xml 基础上加入(或直接查看源码)

<dependency>
  <groupId>org.glassfish.jersey.media</groupId>
  <artifactId>jersey-media-json-jackson</artifactId>
</dependency>
2.1 首先自定义QQAuthenticationFilter过滤器继承AbstractAuthenticationProcessingFilter类
package com.bootcap.session.security.filter;

/**
 * QQ认证过滤器
 * 2018-12-18 10:57
 */
public class QQAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    /**
     * response_type 返回类型
     */
    private final static String RESPONSE_TYPE = "code";

    /**
     * grant_type 由腾讯提供
     */
    private final static String GRANT_TYPE = "authorization_code";

    /**
     * client_id 由腾讯提供(即AppId)
     */
    static final String CLIENT_ID = "101364240";

    /**
     * client_secret 由腾讯提供(即App Key)
     */
    private final static String CLIENT_SECRET = "ef27b7a6ca651a3609dd47f21e385955";

    /**
     * redirect_uri 腾讯回调地址
     */
    private final static String REDIRECT_URI = "http://127.0.0.1/login/qq";

    /**
     * 获取 access_token_url 的 API
     */
    private final static String ACCESS_TOKEN_URL = "https://graph.qq.com/oauth2.0/token";

    /**
     * 获取 OpenID url地址
     */
    private final static String OPENID_URL = "https://graph.qq.com/oauth2.0/me?access_token=%s";

    /**
     * 获取 token 的地址拼接
     */
    private final static String TOKEN_ACCESS_API = "%s?grant_type=%s&client_id=%s&client_secret=%s&code=%s&redirect_uri=%s";

    public QQAuthenticationFilter(String defaultFilterProcessesUrl){
        super(new AntPathRequestMatcher(defaultFilterProcessesUrl, "GET"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
        UsernamePasswordAuthenticationToken authenticationToken =  null;
        String code = httpServletRequest.getParameter(RESPONSE_TYPE);
//        System.out.println("输出response_type:" + code);
        String accessTokenURL = String.format(TOKEN_ACCESS_API,ACCESS_TOKEN_URL,GRANT_TYPE,CLIENT_ID,CLIENT_SECRET,code,REDIRECT_URI);
//        System.out.println("accessTokenURL:" +accessTokenURL);
        QQAccessToken qqAccessToken = getQQAccessToken(accessTokenURL);
        if (null != qqAccessToken){
            String openId = getOpenId(qqAccessToken.getAccessToken());
//            System.out.println("输出openId:" + openId);
            if (null != openId){
                authenticationToken = new UsernamePasswordAuthenticationToken(qqAccessToken.getAccessToken(),openId);
            }
        }
        // 返回验证结果
        return this.getAuthenticationManager().authenticate(authenticationToken);
    }

    /**
     * 开始请求获取QQToken
     * @return
     */
    private QQAccessToken getQQAccessToken(String accessTokenURL){
        Assert.notNull(accessTokenURL,"accessTokenURL不能为空");
        RestTemplate template = new RestTemplate();
        QQAccessToken qqAccessToken = new QQAccessToken();
        String[] results = template.getForObject(accessTokenURL, String.class).split("&");
        if (results.length == 3){
            String accessToken = results[0].replace("access_token=", "");
            Integer expiresIn = Integer.valueOf(results[1].replace("expires_in=", ""));
            String refreshToken = results[2].replace("refresh_token=", "");
            qqAccessToken.setAccessToken(accessToken);
            qqAccessToken.setExpiresIn(expiresIn);
            qqAccessToken.setRefreshToken(refreshToken);
        }
        return qqAccessToken;
    }

    /**
     * 获取用户的唯一OpenId
     * @return
     */
    private String getOpenId(String accessToken){
        RestTemplate template = new RestTemplate();
        String openIdResult = template.getForObject(String.format(OPENID_URL,accessToken), String.class);
        Pattern pattern = Pattern.compile("\"openid\":\"(.*)\"");
        Matcher matcher = pattern.matcher(openIdResult);
        while (matcher.find()){
            return matcher.group(1);
        }
        return null;
    }
}

为什么要继承AbstractAuthenticationProcessingFilter类,请前往Spring Security(二) - 基于数据库实现校验
的流程图。

2.2 创建QQAuthenticationManager通过accessToken和openId获取基本信息,并生成新的Authentication认证对象
package com.bootcap.session.security.manager;

/**
 * 管理授权类
 * 2018-12-18 14:47
 */
public class QQAuthenticatioManager implements AuthenticationManager {

    private static final List<GrantedAuthority> AUTHORITIES = new ArrayList<>();

    public QQAuthenticatioManager(){}

    /**
     * 获取用户信息
     */
    private final static String USER_INFO_URL = "https://graph.qq.com/user/get_user_info?access_token=%s&oauth_consumer_key=%s&openid=%s";

    /**
     * client_id 由腾讯提供(即AppId)
     */
    static final String CLIENT_ID = "101364240";

    static {
        AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        QQUserInfo qqUserInfo = null;
        if (authentication.getName() != null && authentication.getCredentials() != null){
            qqUserInfo = getUserInfo(authentication.getName(), (String) authentication.getCredentials());
        }
//        System.out.println("输出用户信息:" +qqUserInfo.toString());
        return new UsernamePasswordAuthenticationToken(qqUserInfo,null,AUTHORITIES);
    }

    /**
     * 获取QQ授权后的基本信息
     * @param accessToken
     * @param openId
     * @return
     */
    private QQUserInfo getUserInfo(String accessToken, String openId) {
        String url = String.format(USER_INFO_URL,accessToken,CLIENT_ID,openId);
        RestTemplate template = new RestTemplate();
        String userInfoResult = template.getForObject(url, String.class);
        QQUserInfo qqUserInfo = jsonToObject(userInfoResult, QQUserInfo.class);
        return qqUserInfo;
    }

    private <T> T jsonToObject(String json,Class<T> targetClass){
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        try {
            return objectMapper.readValue(json,targetClass);
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
}

为什么要实现AuthenticationManager类?因为在AbstractAuthenticationProcessingFilter.doFilter()方法中调用了this.attemptAuthentication(request, response) 中实现的方法(即2.1中我们自定义的Filter类的实现方法)。因此在我们自定义类中中调用了 this.getAuthenticationManager().authenticate(authRequest),所以需要实现该类。

2.3 在WebSecurityConfig类中添加自定义过滤器
package com.bootcap.session.security.configuration;

/**
 * WebSecurityConfig 配置文件
 * 2018-12-10 11:03
 */
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/user/**").hasRole("USER")
                .and()
                .formLogin().loginPage("/login").defaultSuccessUrl("/user")
                .and()
                .logout().logoutUrl("/logout").logoutSuccessUrl("/login");
        // 在 UsernamePasswordAuthenticationFilter 前添加 QQAuthenticationFilter
        http.addFilterAt(qqAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
                auth.inMemoryAuthentication() // 在内存中进行身份验证
                .passwordEncoder(new BCryptPasswordEncoder())
                .withUser("user")
                .password(new BCryptPasswordEncoder().encode("123456"))
                .roles("USER");
    }

    /**
     * 自定义 QQ登录 过滤器
     */
    private QQAuthenticationFilter qqAuthenticationFilter(){
        QQAuthenticationFilter authenticationFilter = new QQAuthenticationFilter("/login/qq");
        SimpleUrlAuthenticationSuccessHandler simpleUrlAuthenticationSuccessHandler = new SimpleUrlAuthenticationSuccessHandler();
        simpleUrlAuthenticationSuccessHandler.setAlwaysUseDefaultTargetUrl(true);
        simpleUrlAuthenticationSuccessHandler.setDefaultTargetUrl("/user");
        authenticationFilter.setAuthenticationSuccessHandler(simpleUrlAuthenticationSuccessHandler);
        authenticationFilter.setAuthenticationManager(new QQAuthenticatioManager());
        return authenticationFilter;
    }
}
2.4 修改Controller和页面

2.4.1 Controller配置详见【项目代码spring-security-05
2.4.2 login.html页面

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录页面</title>
</head>
<body>
<h1>登录页面</h1>
<div th:if="${param.error}">
    用户名或密码不正确
</div>
<div th:if="${param.logout}">
    你已经退出登录
</div>
<form th:action="@{/login}" method="post">
    <div><label> 用户名: <input type="text" name="username"/> </label></div>
    <div><label> 密&nbsp;&nbsp;&nbsp;码: <input type="password" name="password"/> </label></div>
    <br/>
    <div>
        <input type="submit" value="登录"/>
        <br/>
        <a  href="https://graph.qq.com/oauth2.0/authorize?response_type=code&client_id=101364240&redirect_uri=http://127.0.0.1/login/qq&state=test"><img style="padding-top: 5px" src="/static/imgs/qqLogin.png" /></a>
    </div>
</form>
</body>
</html>

2.4.3 user.html页面

<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>用户界面</title>
</head>
<body>
<div class="container" style="margin-top: 60px">

    <div style="text-align: center; margin-top: 10%">
        <img th:src="${avatar}" th:alt="${avatar}" />
        <p th:text="${username}" style="margin-top: 25px; font-size: 20px; color: crimson"></p>
        <form th:action="@{/logout}" method="post">
            <button style="margin-top: 20px">退出登录</button>
        </form>
    </div>

</div>

</body>

</html>
2.5 补充示例中需要用到的实体类

2.5.1 QQAccessToken.java

package com.bootcap.session.security.entity;

/**
 * 2018-12-18 11:47
 */
public class QQAccessToken {

    /**
     * 授权accessToken
     */
    private String accessToken;

    /**
     * 该access token的有效期,单位为秒。
     */
    private Integer expiresIn;

    /**
     * 在授权自动续期步骤中,获取新的Access_Token时需要提供的参数。
     */
    private String refreshToken;

    public String getAccessToken() {
        return accessToken;
    }

    public void setAccessToken(String accessToken) {
        this.accessToken = accessToken;
    }

    public int getExpiresIn() {
        return expiresIn;
    }

    public void setExpiresIn(Integer expiresIn) {
        this.expiresIn = expiresIn;
    }

    public String getRefreshToken() {
        return refreshToken;
    }

    public void setRefreshToken(String refreshToken) {
        this.refreshToken = refreshToken;
    }

    @Override
    public String toString() {
        return "QQAccessToken{" +
                "accessToken='" + accessToken + '\'' +
                ", expiresIn=" + expiresIn +
                ", refreshToken='" + refreshToken + '\'' +
                '}';
    }
}

2.5.2 QQUserInfo.java

package com.bootcap.session.security.entity;

/**
 * qq信息实体
 * 2018-12-18 15:34
 */
public class QQUserInfo {
    /**
     * 昵称
     */
    private String nickname;

    /**
     * 性别
     */
    private String gender;

    /**
     * QQ头像
     */
    private String figureurl_qq_1;

    public String getNickname() {
        return nickname;
    }

    public void setNickname(String nickname) {
        this.nickname = nickname;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    public String getFigureurl_qq_1() {
        return figureurl_qq_1;
    }

    public void setFigureurl_qq_1(String figureurl_qq_1) {
        this.figureurl_qq_1 = figureurl_qq_1;
    }

    @Override
    public String toString() {
        return "QQUserInfo{" +
                "nickname='" + nickname + '\'' +
                ", gender='" + gender + '\'' +
                ", figureurl_qq_1='" + figureurl_qq_1 + '\'' +
                '}';
    }
}

三、测试

3.1 启动项目并且浏览器访问:127.0.0.1/user,会自动跳转到登录页面



【注意】:我们发现我们使用的静态资源qq登录的图片,不会显示,同时浏览器控制台报404,说图片未找到。
【解决方法】:在configuration包下新建一个WebMvcConfig.java类设置我们静态资源的路径

package com.bootcap.session.security.configuration;

@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
    }
}

3.2 再次启动并访问页面,已经可以正常显示


3.3 使用qq登录


上一篇:Spring Security 入门教程(四)- 权限动态修改

上一篇 下一篇

猜你喜欢

热点阅读