Spring-Security-教程-(五)---实现QQ授权登
一、准备
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> 密 码: <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登录