一、什么是token?
1.token就是身份认证的令牌
我们在前端提交账号和密码到服务端去认证,如果认证成功,服务器会颁发给我们一个令牌,也就是token,待我们下一次再访问一个需要认证的页面时,我们不需要再次输入账号和密码去登录验证,而是通过token去验证,token通常存于客户端的内存中,在用户发起请求时从内存中提取token,并添加到请求参数中即可完成身份认证。
2.token的不同类型
小程序中使用的token接近于通常使用的cookie,还有一种token是经过JWT产生的令牌。JWT令牌可能安全性更高,但也都大同小异。
二、为什么要使用token?
1.移动端(手机端)不对cookies进行维护
我们通常在浏览器登录时会用到cookies,它也是一种身份识别码,它也可以完成登录态保持,但是手机端不对cookies进行维护,当然你可以传过来,保存在你的内存中强制让它为你提供认证服务,但是它毕竟不是原配,也不符合移动端应用的特点,我可能在一个公司的多个应用之间进行切换,使用cookies是不利于传输的。
2.token不存于内存中
传统的cookies保存在内存中,这样会带来几个问题。
- 内存中的数据是关机即丢失的。
- 内存容量通常较小,如果一直将数据存在内存中,很容易导致内存溢出。
三、如何使用token?
要在小程序中获取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并提交了请求。返回结果是成功的。
教程到此结束,谢谢!