本章节基于:四、Springboot 整合Shiro---02认证---记住我
在开始之前,先要理解一下oauth2:
推荐去看一下(六、授权码模式):阮一峰讲解的oauth2
下面附上一张阮一峰博客上的一张截图:
接下来为QQ第三方登陆开发做准备:
一、注册成为开发者
http://wiki.connect.qq.com/%E6%88%90%E4%B8%BA%E5%BC%80%E5%8F%91%E8%80%85
二、创建应用
http://wiki.connect.qq.com/__trashed-2
三、开发前准备
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、单击登陆
2、单击图像,确定授权
4、自动跳转登陆
到此QQ第三方登陆就完成了。
项目地址:springboot-example05
QQ交流群:674851976