H5页面使用微信网页授权实现登录认证

    在用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);
    }
}

完!!!

你可能感兴趣的:(技术实践,H5,网页授权,微信,登录认证,cookie)