介绍在服务端使用 SpringBoot + Shiro ,用户端使用 jQuery 的环境下如何实现网站对接微博登录
http://asing1elife.com/teamnote/social/weibo/callback
127.0.0.1 asing1elife.com
http://127.0.0.1:8080/teamnote
才能访问的网站http://asing1elife.com/teamnote
进行访问了127.0.0.1
访问网站,无法通过映射的域名访问public class AuthorizeRequest {
// 申请应用时分配的AppKey
private String client_id = "";
// 授权回调地址,站外应用需与设置的回调地址一致
private String redirect_uri = "";
// 省略 Getter/Setter
}
scope
参数,在用户扫码确认时,想要获取例如邮箱信息、关注列表等权限就是通过该字段进行指定public class AuthorizeRequestExt extends AuthorizeRequest {
// 申请scope权限所需参数,可一次申请多个scope权限,用逗号分隔
// 可参考 https://open.weibo.com/wiki/Scope
private String scope;
// 用于保持请求和回调的状态,在回调时,会在Query Parameter中回传该参数
private String state;
// 授权页面的终端类型,取值见下面的说明
// default是web浏览器,可选mobile、wap、client、apponweibo
private String display;
// 是否强制用户重新登录,默认false
private boolean forcelogin;
// 授权页语言,缺省为中文简体版,en为英文版
private String language;
// 省略 Getter/Setter
}
private String truncateClassToParams(Object obj) {
// 获取自身
Class<?> clazz = obj.getClass();
// 获取父类
Class<?> superClass = clazz.getSuperclass();
// 先将自身的字段全部获取
List<Field> fields = Lists.newArrayList(clazz.getDeclaredFields());
// 再尝试获取父类字段
if (superClass != null) {
fields.addAll(Arrays.asList(superClass.getDeclaredFields()));
}
StringBuilder paramsBuilder = new StringBuilder();
for (Field field : fields) {
// 允许字段可读取
field.setAccessible(true);
String name = field.getName();
String value = null;
try {
// 尝试获取字段的值
Object valueObj = field.get(obj);
// 值不为空则获取
if (valueObj != null) {
value = valueObj.toString();
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
// 非空的值就拼接到参数中
if (value != null) {
paramsBuilder.append(String.format("%s=%s&", name, value));
}
}
String paramsTemp = paramsBuilder.toString();
// 添加前缀和去掉多余尾缀
return String.format("?%s", paramsTemp.substring(0, paramsTemp.length() - 1));
}
window.open(url, '_self')
直接替换当前页面private static final String WEIBO_AUTHORIZE_URL = "https://api.weibo.com/oauth2/authorize";
public String getAuthorizeUrl() {
String params = truncateClassToParams(new AuthorizeRequestExt());
return WEIBO_AUTHORIZE_URL + params;
}
@Controller
@RequestMapping("/social/weibo")
public class SocialWeiboController {
@Autowire
private SocialWeiboService service;
@GetMapping("/callback")
public String callback(Model model) {
return service.getWeiboAuthorizeCode(model);
}
}
public class AuthorizeResponse {
// 如果授权请求中传递了该参数,会回传该参数
private String state;
// 省略 Getter/Setter
}
public class AuthorizeResponseSuccess extends AuthorizeResponse {
// 用于第二步调用oauth2/access_token接口,获取授权后的access token
private String code;
// 省略 Getter/Setter
}
public class AuthorizeResponseFail extends AuthorizeResponse {
// 错误信息
private String error;
// 错误编码
// 参考文档,页面最底端,https://open.weibo.com/wiki/授权机制说明
// 如果用户在授权页面点了取消,会返回 access_denied:21330
private String error_code;
// 省略 Getter/Setter
}
code
参数,说明请求是成功的,否则就是失败的@Service
public class SocialWeiboService {
@Autowire
private HttpServletRequest request;
public String getWeiboAuthorizeCode(Model model) {
String code = request.getParameter("code");
String state = request.getParameter("state");
if (code == null) {
String error = request.getParameter("error");
String error_code = request.getParameter("error_code");
AuthorizeResponseFail responseFail = new AuthorizeResponseFail();
responseFail.setState(state);
responseFail.setError(error);
responseFail.setError_code(error_code);
return responseFail;
}
AuthorizeResponseSuccess responseSuccess = new AuthorizeResponseSuccess();
responseSuccess.setState(state);
responseSuccess.setCode(code);
return responseSuccess;
}
}
request.getParameter("code")
是否能获取到内容来判断扫码是否成功@Service
public class SocialWeiboService {
public String gerWeiboUser() {
// 这就是上一步骤中封装扫码回执参数的方法
AuthorizeResponse authorizeResponse = getAuthorizeResponse();
// 根据回执的类型判断回执是否成功
if (authorizeResponse instanceof AuthorizeResponseSuccess) {
// 如果成功则可以进行下一步操作
// 如果用户登录成功,就重定向到网站登录之后的首页
return "";
} else {
// 失败的处理方式一般就直接返回首页好了
return "redirect:/index";
}
}
}
code
,但是这个参数本质上的用处是为了下一步用来获取用户凭证code
作为微博用户的凭证,code
本身只是一个临时回执,时效性非常短AuthorizeRequest
,因为参数是通用的public class AccessTokenRequest extends AuthorizeRequest {
// 申请应用时分配的AppSecret
private String client_secret = "";
// 请求的类型
private String grant_type = "authorization_code";
// 调用authorize获得的code值
private String code;
// 省略 Getter/Setter
}
public class AccessTokenResponse {
// 用户授权的唯一票据,用于调用微博的开放接口,
// 同时也是第三方应用验证微博用户登录的唯一票据,
// 第三方应用应该用该票据和自己应用内的用户建立唯一影射关系,来识别登录状态,
// 不能使用本返回值里的UID字段来做登录识别
private String access_token;
// access_token的生命周期,单位是秒数
private String expires_in;
// 授权用户的UID,本字段只是为了方便开发者,减少一次user/show接口调用而返回的
// 第三方应用不能用此字段作为用户登录状态的识别,只有access_token才是用户授权的唯一票据
private String uid;
// 省略 Getter/Setter
}
private String getHttpRequest(String url, String... methodType) {
HttpClient httpClient = new HttpClient();
HttpMethod method;
// 默认使用 Get
if (methodType.length != 0) {
method = new PostMethod(url);
} else {
method = new GetMethod(url);
}
// 失败后尝试重连3次
method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler());
String result = "";
try {
int statusCode = httpClient.executeMethod(method);
result = method.getResponseBodyAsString();
if (statusCode != 200) {
throw new Exception("数据获取失败,statusCode -> " + statusCode);
}
result = method.getResponseBodyAsString();
} catch (Exception e) {
e.printStackTrace();
} finally {
method.releaseConnection();
}
return result;
}
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.util.JSONPObject;
import org.apache.commons.lang3.StringUtils;
public class JsonMapperUtil {
private static Logger logger = LoggerFactory.getLogger(JsonMapperUtil.class);
private ObjectMapper mapper;
public JsonMapperUtil() {
this(Include.ALWAYS);
}
public JsonMapperUtil(Include include) {
mapper = new ObjectMapper();
// 设置输出时包含属性的风格
mapper.setSerializationInclusion(include);
// 设置输入时忽略在JSON字符串中存在但Java对象实际没有的属性
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}
public <T> T fromJson(String jsonString, Class<T> clazz) {
if (StringUtils.isEmpty(jsonString)) {
return null;
}
try {
return mapper.readValue(jsonString, clazz);
} catch (IOException e) {
return null;
}
}
}
private static final String WEIBO_ACCESS_TOKEN_URL = "https://api.weibo.com/oauth2/access_token";
private AccessTokenResponse getAccessToken(String code) {
AccessTokenRequest accessTokenRequest = new AccessTokenRequest();
accessTokenRequest.setCode(code);
String params = truncateClassToParams(accessTokenRequest);
String url = WEIBO_ACCESS_TOKEN_URL + params;
String result = getHttpRequest(url, "POST");
return new JsonMapperUtil().fromJson(result, AccessTokenResponse.class);
}
public class CustomUsernamePasswordToken extends UsernamePasswordToken {
private String mode;
// 省略 Getter/Setter
}
HashedCredentialsMatcher
的 doCredentialsMatch
方法,从自定义的 Token 中获取传入的登录模式public class DefaultHashedCredentialsMatcher extends HashedCredentialsMatcher {
private static final String LOGIN_WEIBO = "weibo";
@Override
public boolean doCredentialsMatch(AuthenticationToken authenticationToken, AuthenticationInfo info) {
CustomUsernamePasswordToken token = (CustomUsernamePasswordToken) authenticationToken;
if (token.getMode().equals(LOGIN_WEIBO)) {
return true;
}
return super.doCredentialsMatch(token, info);
}
}
HashedCredentialsMatcher
进行了重写,所以肯定需要重新注入一下,才能让重写后的 DefaultHashedCredentialsMatcher
生效@Bean
public HashedCredentialsMatcher credentialsMatcher() {
return new DefaultHashedCredentialsMatcher();
}
@Bean
@DependsOn(value = {"credentialsMatcher"})
public ShiroDatabaseRealm shiroRealm(CredentialsMatcher credentialsMatcher) {
ShiroDatabaseRealm shiroRealm = new ShiroDatabaseRealm();
shiroRealm.setCredentialsMatcher(credentialsMatcher);
return shiroRealm;
}
private static final String LOGIN_WEIBO = "weibo";
protected void doSocialLogin(String token) {
CustomUsernamePasswordToken authorizeToken = new CustomUsernamePasswordToken(token, null, LOGIN_WEIBO);
SecurityUtils.getSubject().login(authorizeToken);
}
public class CustomShiroRealm extends AuthorizingRealm {
@Autowired
private UserAuthService userAuthService;
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException {
CustomUsernamePasswordToken token = (CustomUsernamePasswordToken) authcToken;
token.setRememberMe(true);
// 获取用户信息
ShiroPrincipal user = userAuthService.getPrincipal(token.getUsername(), token.getMode());
// 用户不会空
if (user != null) {
// 解码混淆值
byte[] salt = shiroAuthService.getSalt(user.getSalt());
return new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(salt), getName());
} else {
throw new UnknownAccountException("900");
}
}
}
public class UserAuthService {
private static final String LOGIN_WEIBO = "weibo";
@Autowired
private UserService userService;
public ShiroPrincipal getPrincipal(String name, String mode) {
if (mode.equals(LOGIN_WEIBO)) {
return userService.getUserByWeibo(name);
}
return userService.getUser(name, mode);
}
}
public class WeiboUser {
// 昵称
private String screen_name;
// 头像,一个图片的在线链接
private String avatar_large;
// 省略 Getter/Setter
}
private final static String GET_WEIBO_USER_URL = "https://api.weibo.com/2/users/show.json?access_token=%s&uid=%s";
public static WeiboUser getWeiboUser(String accessToken, String uId) {
String url = String.format(GET_WEIBO_USER_URL, accessToken, uId);
String userInfoJson = getHttpRequest(url);
return new JsonMapperUtil().fromJson(userInfoJson, WeiboUser.class);
}
doSocialLogin
方法,即可触发 Shiro 的登录判定