Springboot 整合Shiro认证 集成第三方QQ登录

文章作者:itxsl
文章出处:https://blog.csdn.net/xslde_com/article/details/81141738

前言

在开始之前,先要理解一下oauth2:

推荐去看一下(六、授权码模式):阮一峰讲解的oauth2

下面附上一张阮一峰博客上的一张截图:
在这里插入图片描述

一、注册成为开发者

QQ互联-成为开发者

二、创建应用

QQ互联-创建应用

三、开发前准备

1、确定一下几点都完成了

申请appid和appkey

申请appid和appkey的用途

appid:应用的唯一标识。在OAuth2.0认证过程中,appid的值即为oauth_consumer_key的值。

appkey:appid对应的密钥,访问用户资源时用来验证应用的合法性。在OAuth2.0认证过程中,appkey的值即为oauth_consumer_secret的值。

申请地址
http://connect.qq.com/intro/login/

申请流程

  1. 点击页面上的“申请加入”按钮,申请成为开发者;

  2. 申请appid(oauth_consumer_key/client_id)和appkey(auth_consumer_secret/client_secret);

(1)进入 http://connect.qq.com/manage/ 页面,点击“立即添加”,在弹出的对话框中填写网站或应用的详细资料(名称,域名,回调地址);

(2)点击“确定”按钮,提交资料后,获取appid和appkey。

注意:申请appid时,登录的QQ号码将与申请到的appid绑定,后续维护均需要使用该号码。

2、在原有的代码上进行一些添加和修改

application.yml修改(redirect_uri必须与途中箭头所指地址一致)
在这里插入图片描述

server:
  #设置程序启动端口号
  port: 7000
beetl:
  #模板路径
  templatesPath: templates
oauth:
  qq:
    #你的appid
    client_id: 123456789
    #你的appkey
    client_secret: 123456789
    #你接收响应code码地址
    redirect_uri: http://localhost:7000/authorize/qq
    #腾讯获取code码地址
    code_callback_uri: https://graph.qq.com/oauth2.0/authorize
    #腾讯获取access_token地址
    access_token_callback_uri: https://graph.qq.com/oauth2.0/token
    #腾讯获取openid地址
    openid_callback_uri: https://graph.qq.com/oauth2.0/me
    #腾讯获取用户信息地址
    user_info_callback_uri: https://graph.qq.com/user/get_user_info

创建参数获取类:

QQProperties.class
package com.xslde.properties;
 
/**
 * Created by xslde on 2018/7/21
 * QQ第三方登陆参数类
 */
 
public class QQProperties {
 
    private String client_id;
    private String client_secret;
    private String redirect_uri;
    private String code_callback_uri;
    private String access_token_callback_uri;
    private String openid_callback_uri;
    private String user_info_callback_uri;
 
    public String getClient_id() {
        return client_id;
    }
 
    public void setClient_id(String client_id) {
        this.client_id = client_id;
    }
 
    public String getClient_secret() {
        return client_secret;
    }
 
    public void setClient_secret(String client_secret) {
        this.client_secret = client_secret;
    }
 
    public String getRedirect_uri() {
        return redirect_uri;
    }
 
    public void setRedirect_uri(String redirect_uri) {
        this.redirect_uri = redirect_uri;
    }
 
    public String getCode_callback_uri() {
        return code_callback_uri;
    }
 
    public void setCode_callback_uri(String code_callback_uri) {
        this.code_callback_uri = code_callback_uri;
    }
 
    public String getAccess_token_callback_uri() {
        return access_token_callback_uri;
    }
 
    public void setAccess_token_callback_uri(String access_token_callback_uri) {
        this.access_token_callback_uri = access_token_callback_uri;
    }
 
    public String getOpenid_callback_uri() {
        return openid_callback_uri;
    }
 
    public void setOpenid_callback_uri(String openid_callback_uri) {
        this.openid_callback_uri = openid_callback_uri;
    }
 
    public String getUser_info_callback_uri() {
        return user_info_callback_uri;
    }
 
    public void setUser_info_callback_uri(String user_info_callback_uri) {
        this.user_info_callback_uri = user_info_callback_uri;
    }
}

四、获取code码

OAuthProperties.class
package com.xslde.properties;
 
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
 
/**
 * Created by xslde on 2018/7/21
 */
@Component//注入到spring容器,方便后面使用
@ConfigurationProperties(prefix = "oauth")//对应application.yml中,oauth下参数
public class OAuthProperties {
 
    //获取applicaiton.yml下qq下所有的参数
    private QQProperties qq = new QQProperties();
 
    public QQProperties getQQ() {
        return qq;
    }
 
    public void setQQ(QQProperties qq) {
        this.qq = qq;
    }
}

修改ShiroConf.class

在ShiroConf中添加/login/qq和/authorize/qq可匿名访问

package com.xslde.configurer;
 
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.mgt.WebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
import java.util.LinkedHashMap;
import java.util.Map;
 
/**
 * @Author xslde
 * @Description
 * @Date 2018/7/20 16:25
 */
@Configuration
public class ShiroConf {
 
 
    //注入shiro过滤器
    @Bean("shiroFilterFactoryBean")
    public ShiroFilterFactoryBean shiroFilter(WebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 必须设置 SecurityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        shiroFilterFactoryBean.setLoginUrl("/login");  // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
        shiroFilterFactoryBean.setSuccessUrl("/index");// 登录成功后要跳转的链接
        Map<String, String> chains = new LinkedHashMap<>();
        chains.put("/logout","logout");//登出
        chains.put("/login", "anon");//anon表示可以匿名访问
        chains.put("/login/qq", "anon");//anon表示可以匿名访问
        chains.put("/authorize/qq", "anon");//anon表示可以匿名访问
        //chains.put("/**", "authc");//表示需要认证,才能访问
        chains.put("/**", "user");//表示需要认证或记a住我都能访问
        shiroFilterFactoryBean.setFilterChainDefinitionMap(chains);
        return shiroFilterFactoryBean;
    }
 
 
    //安全管理器
    @Bean
    public WebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 设置realm.
        securityManager.setRealm(shiroRealm());
        securityManager.setCacheManager(cacheManager());
        securityManager.setRememberMeManager(rememberMeManager());
        return securityManager;
    }
 
    //会话管理器
    @Bean
    public SessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionIdUrlRewritingEnabled(true);
        sessionManager.setGlobalSessionTimeout(1 * 60 * 60 * 1000);
        sessionManager.setDeleteInvalidSessions(true);
        sessionManager.setSessionIdCookie(rememberMeCookie());
        return sessionManager;
    }
 
    //Realm,里面是自己实现的认证和授权业务逻辑
    @Bean
    public ShiroRealm shiroRealm() {
        ShiroRealm shiroRealm = new ShiroRealm();
        shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return shiroRealm;
    }
 
    //缓存管理
    @Bean
    public EhCacheManager cacheManager() {
        EhCacheManager cacheManager = new EhCacheManager();
        cacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
        return cacheManager;
    }
 
    //密码管理
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        credentialsMatcher.setHashAlgorithmName("md5"); //散列算法使用md5
        credentialsMatcher.setHashIterations(2);        //散列次数,2表示md5加密两次
        credentialsMatcher.setStoredCredentialsHexEncoded(true);//启用十六进制存储
        return credentialsMatcher;
    }
 
    //cookie管理
    @Bean
    public SimpleCookie rememberMeCookie() {
        SimpleCookie cookie = new SimpleCookie("rememberMe");
        cookie.setHttpOnly(true);
        cookie.setMaxAge(1 * 60 * 60);
        return cookie;
    }
 
    //记住我
    @Bean
    public CookieRememberMeManager rememberMeManager(){
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(rememberMeCookie());
        //这个地方有点坑,不是所有的base64编码都可以用,长度过大过小都不行,没搞明白,官网给出的要么0x开头十六进制,要么base64
        cookieRememberMeManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));
    return cookieRememberMeManager;
    }
}

在LoginAction.class中添加登陆接口
    @Autowired
    private OAuthProperties oauth;
 
 
    //QQ登陆对外接口,只需将该接口放置html的a标签href中即可
    @GetMapping("/login/qq")
    public void loginQQ(HttpServletResponse response) {
        try {
            response.sendRedirect(oauth.getQQ().getCode_callback_uri() + //获取code码地址
                    "?client_id=" + oauth.getQQ().getClient_id()//appid
                    + "&state=" + UUID.randomUUID() + //这个说是防攻击的,就给个随机uuid吧
                    "&redirect_uri=" + oauth.getQQ().getRedirect_uri() +//这个很重要,这个是回调地址,即就收腾讯返回的code码
                    "&response_type=code");//授权模式,授权码模式
        } catch (IOException e) {
            e.printStackTrace();
        }
 
    }

在login.html中添加QQ登陆

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录页面</title>
</head>
<body>
<form action="/login" method="post">
    <div>
        <span style="color: red;">${msg!}</span>
        <br>
        <div>
            <label>用户名称:</label>
            <input type="text" name="username" placeholder="请输入用户名称!">
        </div>
        <br>
        <div>
            <label>用户密码:</label>
            <input type="password" name="password" placeholder="请输入用户密码!">
        </div>
        <div>
            <input type="checkbox" checked="checked"  name="rememberMe" />记住我
        </div>
        <br>
        <input type="submit" value="登录" style="margin-left: 100px">
        <br>
        <span>其他登陆</span><br>
        <a href="/login/qq">QQ登陆</a>
    </div>
</form>
</body>
</html>

启动项目:
在这里插入图片描述
点击QQ登陆 ,会跳转到授权页面
在这里插入图片描述
授权通过后会根据你的回调地址返回code
在这里插入图片描述
到这一步已经成功拿到code了,下一步就是获取access_token了

五、获取access_token、openid、用户信息

新建两个openidDTO:

package com.xslde.model.dto;
 
/**
 * Created by xslde on 2018/7/7
 */
public class QQOpenidDTO {
 
    private String openid;
 
    private String client_id;
 
    public String getOpenid() {
        return openid;
    }
 
    public void setOpenid(String openid) {
        this.openid = openid;
    }
 
    public String getClient_id() {
        return client_id;
    }
 
    public void setClient_id(String client_id) {
        this.client_id = client_id;
    }
}

package com.xslde.model.dto;
 
/**
 * Created by xslde on 2018/7/7
 */
public class QQDTO {
 
    private String ret;        //返回码
    private String msg;        //如果ret<0,会有相应的错误信息提示,返回数据全部用UTF-8编码。
    private String nickname;             //用户在QQ空间的昵称。
    private String figureurl;              //大小为30×30像素的QQ空间头像URL。
    private String figureurl_1;                //大小为50×50像素的QQ空间头像URL。
    private String figureurl_2;                //大小为100×100像素的QQ空间头像URL。
    private String figureurl_qq_1;                   //大小为40×40像素的QQ头像URL。
    private String figureurl_qq_2;                   //大小为100×100像素的QQ头像URL。需要注意,不是所有的用户都拥有QQ的100x100的头像,但40x40像素则是一定会有。
    private String gender;           //性别。 如果获取不到则默认返回"男"
    private String is_yellow_vip;                  //标识用户是否为黄钻用户(0:不是;1:是)。
    private String vip;        //标识用户是否为黄钻用户(0:不是;1:是)
    private String yellow_vip_level;                     //黄钻等级
    private String level;          //黄钻等级
    private String is_yellow_year_vip;                       //标识是否为年费黄钻用户(0:不是; 1:是)
 
    public String getRet() {
        return ret;
    }
 
    public void setRet(String ret) {
        this.ret = ret;
    }
 
    public String getMsg() {
        return msg;
    }
 
    public void setMsg(String msg) {
        this.msg = msg;
    }
 
    public String getNickname() {
        return nickname;
    }
 
    public void setNickname(String nickname) {
        this.nickname = nickname;
    }
 
    public String getFigureurl() {
        return figureurl;
    }
 
    public void setFigureurl(String figureurl) {
        this.figureurl = figureurl;
    }
 
    public String getFigureurl_1() {
        return figureurl_1;
    }
 
    public void setFigureurl_1(String figureurl_1) {
        this.figureurl_1 = figureurl_1;
    }
 
    public String getFigureurl_2() {
        return figureurl_2;
    }
 
    public void setFigureurl_2(String figureurl_2) {
        this.figureurl_2 = figureurl_2;
    }
 
    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;
    }
 
    public String getFigureurl_qq_2() {
        return figureurl_qq_2;
    }
 
    public void setFigureurl_qq_2(String figureurl_qq_2) {
        this.figureurl_qq_2 = figureurl_qq_2;
    }
 
    public String getGender() {
        return gender;
    }
 
    public void setGender(String gender) {
        this.gender = gender;
    }
 
    public String getIs_yellow_vip() {
        return is_yellow_vip;
    }
 
    public void setIs_yellow_vip(String is_yellow_vip) {
        this.is_yellow_vip = is_yellow_vip;
    }
 
    public String getVip() {
        return vip;
    }
 
    public void setVip(String vip) {
        this.vip = vip;
    }
 
    public String getYellow_vip_level() {
        return yellow_vip_level;
    }
 
    public void setYellow_vip_level(String yellow_vip_level) {
        this.yellow_vip_level = yellow_vip_level;
    }
 
    public String getLevel() {
        return level;
    }
 
    public void setLevel(String level) {
        this.level = level;
    }
 
    public String getIs_yellow_year_vip() {
        return is_yellow_year_vip;
    }
 
    public void setIs_yellow_year_vip(String is_yellow_year_vip) {
        this.is_yellow_year_vip = is_yellow_year_vip;
    }
}

接下来需要用到Http工具:
在pom中添加以下依赖:

        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpasyncclient</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpmime</artifactId>
        </dependency>
        <!--json转换工具-->
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.8.5</version>
        </dependency>

在ShiroRealm中添加一个第三方模拟用户:

package com.xslde.configurer;
 
import com.xslde.model.mapped.User;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
 
/**
 * @Author xslde
 * @Description
 * @Date 2018/7/20 16:30
 */
public class ShiroRealm extends AuthorizingRealm {
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }
 
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //获取用户名
        String username = (String) token.getPrincipal();
        //开发中,这里都是去数据库查询
        //做demo,就不查询了
        if (!"xslde".equals(username)&&!"test".equals(username)&&!"xslde.com".equals(username)){
            throw new UnknownAccountException("用户不存在!");
        }
        User user =null;
        if ("xslde".equals(username)){
            user = new User();
            user.setUsername("xslde");
            user.setPassword("0caf568dbf30f5c33a13c56b869259fc");
            user.setSalt("abcd");
            user.setAvailable(1);
        }
        if ("test".equals(username)){
            user = new User();
            user.setUsername("test");
            user.setPassword("0caf568dbf30f5c33a13c56b869259fc");
            user.setSalt("abcd");
            user.setAvailable(0);
        }
 
        //这是模拟数据库里面拥有QQ第三方用户信息
        if ("xslde.com".equals(username)){
            user = new User();
            user.setUsername("xslde.com");
            user.setAvailable(1);
            user.setSalt("abcd");
            user.setPassword("6e20337c6b222fa0a8c3bbb9dd979374");//md5加密后的密文,这里是用的openid做密码
        }
        if (user.getAvailable()!=1){
            throw  new LockedAccountException("账户已被锁定");
        }
        return  new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(user.getSalt()), getName());
    }
 
    //生成一个加盐密码
    public static void main(String[] args) {
        String hashAlgorithmName = "md5";//加密类型
        Integer iteration = 2;//迭代次数
        String password = "123456";
        String salt = "abcd";
        String s = new SimpleHash(hashAlgorithmName,password,salt,iteration).toHex();
        System.out.println(s);
        //加密后的密码
        //0caf568dbf30f5c33a13c56b869259fc
 
    }
}

在LoginAction.calss中添加:

    //接收回调地址带过来的code码
    @GetMapping("/authorize/qq")
    public String authorizeQQ(Map<String, String> msg, String code) {
        HashMap<String, Object> params = new HashMap<>();
        params.put("code", code);
        params.put("grant_type", "authorization_code");
        params.put("redirect_uri", oauth.getQQ().getRedirect_uri());
        params.put("client_id", oauth.getQQ().getClient_id());
        params.put("client_secret", oauth.getQQ().getClient_secret());
        //获取access_token如:access_token=9724892714FDF1E3ED5A4C6D074AF9CB&expires_in=7776000&refresh_token=9E0DE422742ACCAB629A54B3BFEC61FF
        String result = HttpsUtils.doGet(oauth.getQQ().getAccess_token_callback_uri(), params);
        //对拿到的数据进行切割字符串
        String[] strings = result.split("&");
        //切割好后放进map
        Map<String, String> reulsts = new HashMap<>();
        for (String str : strings) {
            String[] split = str.split("=");
            if (split.length > 1) {
                reulsts.put(split[0], split[1]);
            }
        }
        //到这里access_token已经处理好了
        //下一步获取openid,只有拿到openid才能拿到用户信息
        String openidContent = HttpsUtils.doGet(oauth.getQQ().getOpenid_callback_uri() + "?access_token=" + reulsts.get("access_token"));
        //接下来对openid进行处理
        //截取需要的那部分json字符串
        String openid = openidContent.substring(openidContent.indexOf("{"), openidContent.indexOf("}") + 1);
        Gson gson = new Gson();
        //将返回的openid转换成DTO
        QQOpenidDTO qqOpenidDTO = gson.fromJson(openid, QQOpenidDTO.class);
 
        //接下来说说获取用户信息部分
        //登陆的时候去数据库查询用户数据对于openid是存在,如果存在的话,就不用拿openid获取用户信息了,而是直接从数据库拿用户数据直接认证用户,
        // 否则就拿openid去腾讯服务器获取用户信息,并存入数据库,再去认证用户
        //下面关于怎么获取用户信息,并登陆
        params.clear();
        params.put("access_token", reulsts.get("access_token"));//设置access_token
        params.put("openid", qqOpenidDTO.getOpenid());//设置openid
        params.put("oauth_consumer_key", qqOpenidDTO.getClient_id());//设置appid
        //获取用户信息
        String userInfo = HttpsUtils.doGet(oauth.getQQ().getUser_info_callback_uri(), params);
        QQDTO qqDTO = gson.fromJson(userInfo,QQDTO.class);
        //这里拿用户昵称,作为用户名,openid作为密码(正常情况下,在开发时候用openid作为用户名,再自己定义个密码就可以了)
        try {
            SecurityUtils.getSubject().login(new UsernamePasswordToken(qqDTO.getNickname(), qqOpenidDTO.getOpenid()));
        }catch (Exception e){
            msg.put("msg","第三方登陆失败,请联系管理!");
            logger.error(e.getMessage());
            return "login.html";
        }
        return "redirect:/index";
    }
 

六、这些步骤完成后,启动项目:

1、单击登陆
在这里插入图片描述

在这里插入图片描述

4、自动跳转登陆
在这里插入图片描述

到此QQ第三方登陆就完成了。
项目地址(原作者): https://gitee.com/xslde/springboot-example

猜你喜欢

转载自blog.csdn.net/qq_36698956/article/details/88894258