在用H5开发微信公众号页面应用时,往往需要获取微信的用户信息,H5页面在微信属于访问第三方网页,因此通过微信网页授权机制,来获取用户基本信息,此处需要用户确认授权才能获取,用户确认授权后,我们可以认为用户已经登录了,这时候就需要保留用户登录的凭证,此处可以通过记录cookie来达成目的。
具体流程就是:用户通过微信打开H5页面时,判断用户是否已经在cookie中记录信息,如果已经登录未过期,则直接跳转到业务访问页面,如果未登录或者凭证已过期,则弹出微信网页授权页面,引导用户完成用户授权,待用户确认授权后,后台保存用户信息同时往cookie写入登录凭证,最后跳转到业务访问页面即可。
其中,微信网页授权可参照官方的 微信网页授权 ,下面介绍下我的具体实现和代码:
Controller:
@RequestMapping("/login")
public String login(Model model){
RequestCache requestCache = new HttpSessionRequestCache();
// 重定向前一URL
String url = "/index";
SavedRequest savedRequest = requestCache.getRequest(request,response);
if(savedRequest != null){
url = savedRequest.getRedirectUrl();
}
model.addAttribute("url","/authorize?url="+ url);
}
/**
* authorize?url=index
* @param url
* @return
*/
@RequestMapping("/authorize")
public String authorize(@RequestParam(name="url",required=false)String url){
Cookie cookie = CookieUtils.get(request, cookieName);
if (cookie == null) {
String backUrl = apiDomain + "wxlogin?url="+url;
String redirectUrl = wxService.oauth2AuthorizationUrl(backUrl);
return "redirect:" + redirectUrl;//必须重定向,否则不能成功
}else{
return "redirect:" + url;
}
}
@RequestMapping("/wxlogin")
public String wxLogin(@RequestParam(name="code",required=false)String code,
@RequestParam(name="state",required=false)String state,
@RequestParam(name="url",required=false)String url
) throws AuthenticationException
{
WxUserInfo wxUser = wxService.oauth2getUserInfo(code);
if (wxUser != null) {
// 生成token
WebAccessToken accessToken = new WebAccessToken();
accessToken.setUuid(IDUtils.getUuid());
accessToken.setExpiresIn(cookieMaxAge);
accessToken.setOpenid(wxUser.getOpenid());
// 保存token
Constants.LOG.info("Save web access token is [{}]",accessToken);
wxService.newWebAccessToken(accessToken);
CookieUtils.set(response, cookieName, accessToken.getUuid(), cookieMaxAge);
UserInfo user = userService.getUserByName(wxUser.getOpenid());
if ( user == null) {
// 新用户
user = new UserInfo();
user.setUserName(wxUser.getOpenid());
user.setNickName( EmojiParser.parseToAliases(wxUser.getNickname()) ); //EmojiParser.parseToUnicode(string)
user.setUserPhoto(wxUser.getHeadimgurl());
user.setUserPassword(Constants.DEFAULT_USER_PASSWORD);
user.setOpenid(wxUser.getOpenid());
int userId = userService.newUser(user);
user.setUserId(userId);
Constants.LOG.info("New user from openid [{}], save id is [{}]",wxUser.getOpenid(), userId);
}
}else{
Constants.LOG.error("WxUserInfo is empty.");
throw new AuthenticationServiceException("登录失败: 未获取到微信用户信息");
}
return "redirect:" + url;
}
wxService:
/***********************************************oauth2 api*********************************************************/
public String oauth2AuthorizationUrl(String uri){
String redirectUrl = "";
try {
redirectUrl = oauth2Authorize.replace("APPID", appid)
.replace("REDIRECT_URI", URLEncoder.encode(uri, "UTF-8"))
.replace("SCOPE", "snsapi_userinfo") // snsapi_base
.replace("STATE", "1");
}catch (UnsupportedEncodingException ex){
Constants.LOG.error(ex.getMessage());
}
Constants.LOG.info("Oauth2 authorization url is [{}]",redirectUrl);
return redirectUrl;
}
/**
* oauth2 拉取用户信息
* @param code
* @return
*/
public WxUserInfo oauth2getUserInfo(String code){
WxUserInfo userInfo = null;
String requestUrl = oauth2AccessToken.replace("APPID", appid)
.replace("SECRET",secret)
.replace("CODE",code)
;
try {
Constants.LOG.info("begin oauth2 access token [{}]", requestUrl);
JSONObject jsonObject = restTemplate.getForObject(requestUrl, JSONObject.class);
Constants.LOG.info("finish oauth2 access token [{}], json [{}]", requestUrl, jsonObject);
if(jsonObject.containsKey("errcode")){
Constants.LOG.error("Obtain access token failed [{}]", jsonObject.getString("errmsg"));
}else{
synchronized (this){
WebAccessToken webAccessToken = jsonObject.toJavaObject(WebAccessToken.class);
// 判断access_token是否有效
requestUrl = oauth2Validate.replace("ACCESS_TOKEN", webAccessToken.getAccessToken())
.replace("OPENID",webAccessToken.getOpenid());
Constants.LOG.info("begin oauth2 validate [{}]", requestUrl);
jsonObject = restTemplate.getForObject(requestUrl, JSONObject.class);
Constants.LOG.info("finish oauth2 validate [{}], json [{}]", requestUrl, jsonObject);
if ( !jsonObject.getString("errcode").equals("0") ){
// 刷新access_token
requestUrl = oauth2RefreshToken.replace("ACCESS_TOKEN", webAccessToken.getAccessToken())
.replace("OPENID",webAccessToken.getOpenid());
Constants.LOG.info("begin oauth2 refresh token [{}]", requestUrl);
jsonObject = restTemplate.getForObject(requestUrl, JSONObject.class);
Constants.LOG.info("finish oauth2 refresh token [{}], json [{}]", requestUrl, jsonObject);
if(jsonObject.containsKey("errcode")){
Constants.LOG.error("Refresh access token failed [{}]", jsonObject.getString("errmsg"));
}else{
webAccessToken = jsonObject.toJavaObject(WebAccessToken.class);
}
}
// 获取userinfo
requestUrl = oauth2UserInfo.replace("ACCESS_TOKEN", webAccessToken.getAccessToken())
.replace("OPENID",webAccessToken.getOpenid());
Constants.LOG.info("begin oauth2 user info [{}]", requestUrl);
jsonObject = restTemplate.getForObject(requestUrl, JSONObject.class);
Constants.LOG.info("finish oauth2 user info [{}], json [{}]", requestUrl, jsonObject);
if(jsonObject.containsKey("errcode")){
Constants.LOG.error("Obtain user info failed [{}]", jsonObject.getString("errmsg"));
}else{
userInfo = jsonObject.toJavaObject(WxUserInfo.class);
boolean flag = this.saveUserInfo(userInfo);
Constants.LOG.info("Save wx user info result is [{}]", flag);
}
}
}
} catch (Exception ex){
Constants.LOG.error(ex.getMessage());
}
return userInfo;
}
CookieUtils:
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
public class CookieUtils {
public static void set(HttpServletResponse response,
String name, String value,
int maxAge){
Cookie cookie = new Cookie(name, value);
cookie.setPath("/");
cookie.setMaxAge(maxAge);
response.addCookie(cookie);
}
/**
* 获取Cookie
* @param request
* @param name
* @return
*/
public static Cookie get(HttpServletRequest request,
String name){
Map map = readCookieMap(request);
if(map.containsKey(name)){
return map.get(name);
}else{
return null;
}
}
}
"/authorize?url=URL"是处理用户授权的页面,首先会从cookie中获取登录凭证,即获取键值为cookieName(值是token)的cookie,看是是否存在和过期,如果能获取得到,证明用户不是首次登录,会自动重定向到授权页面前访问的页面URL,如果cookie为空,则重定向到https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=/wxLogin?url=URL&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect,弹出微信的授权页面等待用户确认,如用户确认登录之后,则会重定向redirect_uri,即我所写的/wxLogin?url=URL,此处带上URL的原因就是我想让授权登录后还是返回授权前的页面,让登录的用户体验好点(如果是已登录的用户则直接跳转,做到无感知);
“/wxLogin?url=URL”是“通过code换取网页授权access_token,并做后续的处理”,对应官方的第二到第四步,同时再处理完之后,其中换取网页授权access_token,判断access_token是否过期,刷新access_token,并通过access_token来获取用户的基本信息,都在wxService.oauth2getUserInfo(code)里面,其中的变量参数如下:
@Value("${wx.oauth2.authorize}")
private String oauth2Authorize;
@Value("${wx.oauth2.access_token}")
private String oauth2AccessToken;
@Value("${wx.oauth2.refresh_token}")
private String oauth2RefreshToken;
@Value("${wx.oauth2.userinfo}")
private String oauth2UserInfo;
@Value("${wx.oauth2.validate}")
private String oauth2Validate;
//springboot配置文件中
wx:
oauth2:
authorize: https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
access_token: https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
refresh_token: https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=APPID&grant_type=refresh_token&refresh_token=REFRESH_TOKEN
userinfo: https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN
validate: https://api.weixin.qq.com/sns/auth?access_token=ACCESS_TOKEN&openid=OPENID
获取到用户的基本信息之后,就可以保存一下access_token,同时往cookie里面写入登录凭证,此处写入cookie的格式“token=openid”,以微信的openid来做用户的标识(如果是微信公众H5和微信小程序公用的,微信是建议用UnionID),同时判断用户是否已存在,新用户需要创建用户信息,写入数据库用户表,而老用户则不需要,最后重定向业务访问URL。
到此,则完成微信的网页授权流程,也完成用户的登录操作,至于springboot security的登录认证机制,可以沿用WebSecurity配置,另增加OncePerRequestFilter来识别用户是否登录,代码如下:
SecurityConfig:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// Spring会自动寻找实现接口的类注入,会找到我们的 UserDetailsServiceImpl 类
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private LogoutHandler logoutHandler;
@Autowired
private AuthenticationTokenFilter authenticationTokenFilter;
@Autowired
public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder
// 设置UserDetailsService
.userDetailsService(this.userDetailsService)
// 使用BCrypt进行密码的hash
.passwordEncoder(this.passwordEncoder);
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// 取消csrf
.csrf().disable()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 允许对于网站静态资源的无授权访问
.antMatchers(
HttpMethod.GET,
"/",
"/*.html"
).permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.and()
.logout().logoutSuccessHandler(logoutHandler)
.logoutUrl("/logout");
;
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
}
authenticationTokenFilter:
@Component
public class AuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private WxService wxService;
@Value("${cookie.name}")
private String cookieName;
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
if ( request.getServletPath().contains("login")
|| request.getServletPath().contains("logout")
|| request.getServletPath().contains("authorize")
) {
return true;
}else{
return super.shouldNotFilter(request);
}
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
Cookie cookie = CookieUtils.get(request,cookieName);
String token = cookie != null ? cookie.getValue() : "";
if(StringUtils.isNotBlank(token)){
WebAccessToken accessToken = wxService.getToken(token);
if (accessToken != null){
UserDetails userDetails = userDetailsService.loadUserByUsername(accessToken.getOpenid());
if (userDetails == null){
userDetailsService.loadUserByUsername(accessToken.getCellNo().toString());
}
if (userDetails == null){
throw new AuthenticationServiceException("登录失败:用户不存在");
}
Authentication authentication = new UsernamePasswordAuthenticationToken(
userDetails,null,userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
super.doFilter(request, response, chain);
}
}
完!!!