赞
踩
1.token就是身份认证的令牌
我们在前端提交账号和密码到服务端去认证,如果认证成功,服务器会颁发给我们一个令牌,也就是token,待我们下一次再访问一个需要认证的页面时,我们不需要再次输入账号和密码去登录验证,而是通过token去验证,token通常存于客户端的内存中,在用户发起请求时从内存中提取token,并添加到请求参数中即可完成身份认证。
2.token的不同类型
小程序中使用的token接近于通常使用的cookie,还有一种token是经过JWT产生的令牌。JWT令牌可能安全性更高,但也都大同小异。
1.移动端(手机端)不对cookies进行维护
我们通常在浏览器登录时会用到cookies,它也是一种身份识别码,它也可以完成登录态保持,但是手机端不对cookies进行维护,当然你可以传过来,保存在你的内存中强制让它为你提供认证服务,但是它毕竟不是原配,也不符合移动端应用的特点,我可能在一个公司的多个应用之间进行切换,使用cookies是不利于传输的。
2.token不存于内存中
传统的cookies保存在内存中,这样会带来几个问题。
要在小程序中获取token,首先得看懂微信官方的小程序登录时序图
这里的openid就是我们说的token,前端不能直接从微信服务器获取openid,必须得从开发者服务器来获取,这主要是出于对appid和appsecret这两个信息安全性的考虑,将其通过前端来传输是不安全的,所以必须存于别人看不见的开发者服务器中。
1.获取code,并提交给后台
// pages/demo2/demo2.js Page({ onLoad: function (options) { wx.login({ success(resn){ console.log(resn.code) wx.request({ method:'POST', url: 'http://localhost:8080/login', data:{ username:'user', password:'123', miniCode:resn.code }, header:{ userToken:wx.getStorageSync('userToken'), requestCode:'601', 'content-type':'application/x-www-form-urlencoded' }, success(res){ wx.setStorageSync('userToken', res.data.userToken) console.log(res.data) } }) } }) } })
这里在请求参数中已经添加了账号密码,http://localhost:8080/login是我在后端设定好的登录接口。requestCode:'601’是我设定的一个请求头参数,代表它是登录请求,后台会根据这个code直接跳转到登录过滤器。在后面的代码中可以看到处理方式。
2.获取openid
package com.fiblue.home.login; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; import com.fasterxml.jackson.databind.ObjectMapper; import com.fiblue.home.entity.SuccessJsonMsgPojo; import com.fiblue.home.entity.TokenDto; import com.fiblue.home.entity.Wechat; import com.fiblue.home.msg.SuccessfulJsonMsgProvide; import com.fiblue.home.service.UserTokenService; @Component public class MySuccessHandle extends SimpleUrlAuthenticationSuccessHandler { @Autowired private SuccessJsonMsgPojo loginSuccessInfo; @Autowired private Wechat wechat; @Autowired private ObjectMapper mapper; @Autowired private UserTokenService userTokenService; @Autowired private SuccessfulJsonMsgProvide successfulJsonMsgProvide; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { final Logger log = LoggerFactory.getLogger("success日志:"); String currentUser = authentication.getName(); logger.info("用户" + currentUser + "登录成功"); response.setStatus(200); response.setContentType("application/json; charset=utf-8"); // token处理 String miniCode = request.getParameter("miniCode"); String url = wechat.getUrl() + "&appid=" + wechat.getAppid() + "&secret=" + wechat.getSecret() + "&js_code=" + miniCode + "&grant_type=authorization_code"; String userName=authentication.getName(); String userToken=getToken(url).getOpenid(); userTokenService.updateTokenbyUserName(userName, userToken);//更新token userTokenService.updateTokenTimebyUserName(userName);//更新时间 successfulJsonMsgProvide.printSuccessfulJsonMsg(response, "登录成功", "701", userToken, userName); log.info(getToken(url).getOpenid()); } public TokenDto getToken(String url) throws IOException { RestTemplate restTemplate = new RestTemplate(); String result = restTemplate.getForObject(url, String.class); TokenDto tokenDto = mapper.readValue(result, TokenDto.class); return tokenDto; } }
我们重写了继承并重写了SimpleUrlAuthenticationSuccessHandler,这是一个登录成功后的处理类,它告诉服务器在密码验证成功后该怎么办,默认是跳转到成功页,但是我们这里设定它将openid和其它信息打包成json数据返回给前端并将openid存于数据库中。我已经提前将appid和appsecret等信息存于ymal文件中,直接用一个实体类wechat将其封装起来,这里没必要多说了。
3.token拦截器AuthenticationTokenFilter.class
package com.fiblue.home.filter; import java.io.IOException; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import com.fiblue.home.entity.Wechat; import com.fiblue.home.msg.ErrorJsonMsgProvide; import com.fiblue.home.service.UserService; import com.fiblue.home.service.UserTokenService; @Component public class AuthenticationTokenFilter extends OncePerRequestFilter { private final Logger log = LoggerFactory.getLogger("日志:"); @Autowired private UserService userService; @Autowired private UserTokenService userTokenService; @Autowired private Wechat wechat; @Autowired private ErrorJsonMsgProvide errorJsonMsgProvide; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String requestCode = request.getHeader("requestCode");// 获取请求码,如果requestCode在头信息中不存在则报错,可以为null String userToken = request.getHeader("userToken");// 获取token String miniCode = request.getParameter("miniCode"); String url = wechat.getUrl() + "&appid=" + wechat.getAppid() + "&secret=" + wechat.getSecret() + "&js_code=" + miniCode + "&grant_type=authorization_code"; log.info(url); if (userToken != null && !requestCode.equals("601")) {// 如果token存在且不是登录请求 String userName = userTokenService.loadUserByUsertoken(userToken); boolean valid; if(userName!=null) { valid = userTokenService.checkTokenTimeValidbyUserName(userName); } else { valid=false;//如果用户名不存在,则没必要再进去查询,如果出现空查询则出现错误 } if (userName != null && valid==true) {//非空且有效 如果其中任何一个地方不通过,都应该停止 UserDetails userDetails = userService.loadUserByUsername(userName);// 根据用户名获取用户对象 if (userDetails != null) { log.info(userDetails.getPassword());// UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, userDetails.getPassword(), userDetails.getAuthorities()); // 设置为已登录 SecurityContextHolder.getContext().setAuthentication(authentication); // 更新时间戳 userTokenService.updateTokenTimebyUserName(userName); log.info("用户:"+userName+" 通过token登录成功"); userTokenService.updateTokenTimebyUserName(userName);//更新token时间,即续期 chain.doFilter(request, response);//如果认证通过则doFilter } } else {// 如果用户名查找不到 errorJsonMsgProvide.printErrorJsonMsg(response,"token无效", "704"); } } if (requestCode.equals("601")) { chain.doFilter(request, response);//如果是登录请求也进入doFilter } } }
这是一个用在UsernamePasswordAuthenticationFilter之前的过滤器,我们知道UsernamePasswordAuthenticationFilter是一个账号密码处理的一个过滤器,token就是在密码验证之前进行对比,如果token验证通过,就将用户的完整信息封装到UsernamePasswordAuthenticationToken中,并设置成已经验证通过的类型,然后保存于上下文Context中,最后执行接下来的过滤器,也就是chain.doFilter(request, response)。具体是什么情况,大家可以打开UsernamePasswordAuthenticationToken查super.setAuthenticated(true)这个方法是怎么回事就知道了。至于上下文,其实是在后面的过滤器中是通过上下文读取用户信息的,其中,用户是否验证通过这个信息就很重要。
4.更改配置类
package com.fiblue.home.securityconfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import com.fiblue.home.filter.AuthenticationTokenFilter; import com.fiblue.home.login.MyFailureHandle; import com.fiblue.home.login.MySuccessHandle; @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter{ @Autowired private MySuccessHandle mySuccessHandle; @Autowired private MyFailureHandle myFailureHandle; @Autowired private AuthenticationTokenFilter authenticationTokenFilter; @Override protected void configure(HttpSecurity http) throws Exception { // TODO Auto-generated method stub http .addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class) .csrf().disable() .authorizeRequests() .antMatchers("/", "/index").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login").permitAll() .loginProcessingUrl("/login") .successHandler(mySuccessHandle) .failureHandler(myFailureHandle) .and() .logout().permitAll() .and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); } @Bean public PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } }
我们通过addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)这个方法将token过滤器添加到密码验证过滤器之前,并通过successHandler(mySuccessHandle),改变了密码登录成功后的处理逻辑。
5.测试
1.微信小程序控制台中看到了返回的openid
2.数据库中也更新了token和时间戳
3.请求需要授权的数据
// pages/demo4/demo4.js Page({ onLoad: function (options) { wx.request({ url: 'http://localhost:8080/test', header:{ 'requestCode':'602', 'content-type':'application/x-www-form-urlencoded', 'userToken':wx.getStorageSync('userToken') }, success(res){ console.log(res.data) } }) } })
/test这个地址是需要授权才能访问的,这里通过wx.getStorageSync(‘userToken’)这api接口获取了token并提交了请求。返回结果是成功的。
教程到此结束,谢谢!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。