思路:
微信小程序获取用户授权后能获取到授权code,
前端调用登录接口传给后端code获取用户openid等信息。
登录接口:通过微信api调用getSessionInfo获取openid等基本信息,
获取成功后判断是否为新用户进行登录/新增用户后登录,
通过jwt生成token与refreshToken,将token与refreshToken存入redis中并设置有效期,然后将token与refreshToken返回给前端。
前端每次调用接口在请求头中传入token。
后端使用shiro自定义filter拦截请求,并解析token进行校验,若token校验失败或者过期则返回固定code给前端。
前端请求收到对应code后调用refreh_token接口传入refreshToken获取新的token。
refreh_token接口:接受到refrehToken参数后进行校验,若校验通过则生成新的token并存入redis;若校验失败或者refrehToken过期则返回失败信息,前端接收到失败的信息后重新调用登录接口进行登录。
以下具体代码:
@GetMapping(“login”)
@ApiOperation(“微信登录”)
public AjaxResult login(String code){
try {
WxMaJscode2SessionResult info = wxMaService.getUserService().getSessionInfo(code);
//这里是通过jdk方式调用微信api
ResultToken token = new ResultToken();
User user= userService.queryByOpenId(info.getOpenid());
if(user==null){
//保存新用户
user=new User ();
user.setOpenId(info.getOpenid());
user.insert(user);
}
token.setUserId(viViuser.getId());
String userToken = getToken(viViuser);
String refreshToken = getRefreshToken(viViuser);
token.setToken(userToken);
token.setRefresh_token(refreshToken);
//存入token与refreshToken并设置过期时间,refresh_toke的过期时间至少为token的两倍以上,这里使用指定前缀+userid作为key,token作为value
redisRepository.setExpire(ViConstants.PREFIX_VI_REDIS_USER_TOKEN+viViuser.getId(),userToken,3600*24);
redisRepository.setExpire(ViConstants.PREFIX_VI_REDIS_USER_REFRESH_TOKEN+viViuser.getId(),refreshToken,3600*24*7);
return AjaxResult.success(token);
} catch (WxErrorException e) {
e.printStackTrace();
return AjaxResult.error(e.getError().getErrorMsg());
}
}
public String getToken(User user) {
String token="";
token= JWT.create().withAudience(String.valueOf(user.getId())).withIssuedAt(new Date())
.sign(Algorithm.HMAC256(ViConstants.PREFIX_VI_TOKEN_SIGNuser.getOpenId()));
return token;
}
public String getRefreshToken(User user) {
String token="";
token= JWT.create().withAudience(String.valueOf(user.getId())).withIssuedAt(new Date())
.sign(Algorithm.HMAC256(ViConstants.PREFIX_VI_REFRESH_TOKEN_SIGN+user.getOpenId()));
return token;
}
JWTFilter:
public class JWTFilter extends BasicHttpAuthenticationFilter {
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
//生成shiro中的token并交给shiro进行登录验证,登录验证过程在userRealm进行重写
AuthenticationToken token = this.createToken(servletRequest, servletResponse);
try {
//交给shiro进行登录
this.getSubject(servletRequest, servletResponse).login(token);
} catch (AuthenticationException e) {
//如果登录失败,返回统一的数据类型json
HttpServletResponse httpServletResponse = (HttpServletResponse)servletResponse;
httpServletResponse.setContentType("application/json");
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.getWriter().write(new Gson().toJson(new AjaxResult(AjaxResult.Type.UN_LOGIN,"Unauthorized")) );
return false;
}
return true;
}
/**
* 支持跨域
*
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-control-Allow-Origin", "*");
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", "token,Authorization,Origin,X-Requested-With,Content-Type,Accept");
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(httpServletRequest, httpServletResponse);
}
@Override
protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) {
//获取token,如果是get请求可从param中获取
HttpServletRequest request = (HttpServletRequest) servletRequest;
String token = request.getHeader("token");
if(request.getMethod().equals(RequestMethod.GET.name())&& StringUtils.isEmpty(token)){
token=request.getParameter("token");
}
return new JWTToken(token);
}
}
UserRealm
@Component
public class AppUserRealm extends AuthorizingRealm {
@Autowired
UserService userService ;
@Autowired
private RedisRepository redisRepository;
@Override
public boolean supports(AuthenticationToken token) {
return true;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo auth = new SimpleAuthorizationInfo();
//TODO: 授权管理
return auth;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authToken) throws org.apache.shiro.authc.AuthenticationException {
String token = (String) authToken.getCredentials();
//检查是否有token
if (StringUtils.isEmpty(token)) {
throw new ViAuthenticationException("无token,请重新登录");
}
// 获取 token 中的 user id
String userId;
try {
userId = JWT.decode(token).getAudience().get(0);
} catch (JWTDecodeException j) {
throw new ViAuthenticationException("解析token失败:"+token);
}
ViViuser user = viViuserService.selectViViuserById(Long.valueOf(userId));
if (user == null) {
throw new ViAuthenticationException("用户不存在,请重新登录");
}
// jwt 验证 token
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(ViConstants.PREFIX_VI_TOKEN_SIGN+user.getOpenId())).build();
try {
jwtVerifier.verify(token);
} catch(JWTVerificationException e) {
throw new ViAuthenticationException("jwt校验失败");
}
// 验证 token(redis)
if(!verifyToken(token,user)){
throw new ViAuthenticationException("token过期");
}
AuthenticationInfo authcInfo = new SimpleAuthenticationInfo(user, token, userId);
return authcInfo;
}
boolean verifyToken(String token,User user){
String cacheToken = redisRepository.get(ViConstants.PREFIX_VI_REDIS_USER_TOKEN+ user.getId());
//当传入token与缓存中token相等时,说明token有效
if(!StringUtils.isEmpty(cacheToken)&&cacheToken.equals(token)){
return true;
}else {
return false;
}
}
}
//禁用session的配置类
public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory {
@Override
public Subject createSubject(SubjectContext context) {
//不创建session
context.setSessionCreationEnabled(false);
return super.createSubject(context);
}
}
ShiroConfig
@Configuration
public class ShiroConfig
{
@Bean(name = "subjectFactory")
public StatelessDefaultSubjectFactory subjectFactory() {
StatelessDefaultSubjectFactory statelessDefaultSubjectFactory = new StatelessDefaultSubjectFactory();
return statelessDefaultSubjectFactory;
}
@Bean(name = "sessionManager")
public DefaultSessionManager sessionManager() {
DefaultSessionManager sessionManager = new DefaultSessionManager();
sessionManager.setSessionValidationSchedulerEnabled(false);
return sessionManager;
}
@Bean(name = "defaultSessionStorageEvaluator")
public DefaultSessionStorageEvaluator defaultSessionStorageEvaluator () {
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
return defaultSessionStorageEvaluator;
}
@Bean(name = "subjectDAO")
public DefaultSubjectDAO subjectDAO(@Qualifier("defaultSessionStorageEvaluator")DefaultSessionStorageEvaluator defaultSessionStorageEvaluator) {
DefaultSubjectDAO defaultSubjectDAO = new DefaultSubjectDAO();
defaultSubjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
return defaultSubjectDAO;
}
@Bean(name = "securityManager")
public SecurityManager securityManager(AppUserRealm myRealm, @Qualifier("subjectDAO")DefaultSubjectDAO
subjectDAO, @Qualifier("sessionManager")DefaultSessionManager sessionManager, @Qualifier("subjectFactory")StatelessDefaultSubjectFactory subjectFactory) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myRealm);
securityManager.setSubjectDAO(subjectDAO);
securityManager.setSubjectFactory(subjectFactory);
securityManager.setSessionManager(sessionManager);
return securityManager;
}
public JWTFilter jwtFilter() {
return new JWTFilter();
}
@Bean
public FilterRegistrationBean delegatingFilterProxy(){
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
DelegatingFilterProxy proxy = new DelegatingFilterProxy();
proxy.setTargetFilterLifecycle(true);
proxy.setTargetBeanName("shiroFilter");
filterRegistrationBean.setFilter(proxy);
return filterRegistrationBean;
}
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager")SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//
LinkedHashMap<String, Filter> filters = new LinkedHashMap<>();
//拦截链
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
//swagger
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
filterChainDefinitionMap.put("/swagger-resources/**", "anon");
filterChainDefinitionMap.put("/v2/api-docs", "anon");
filterChainDefinitionMap.put("/webjars/springfox-swagger-ui/**", "anon");
filterChainDefinitionMap.put("/", "anon");
filterChainDefinitionMap.put("/csrf", "anon");
//登录接口
filterChainDefinitionMap.put("/agent/login", "anon");
filterChainDefinitionMap.put("/agent/refresh_token", "anon");
// 对静态资源设置匿名访问
filterChainDefinitionMap.put("/favicon.ico**", "anon");
filters.put("jwtFilter", jwtFilter());
shiroFilterFactoryBean.setFilters(filters);
filterChainDefinitionMap.put("/**", "jwtFilter");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean(name = "advisorAutoProxyCreator")
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean(name = "authorizationAttributeSourceAdvisor")
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
JWTToken
public class JWTToken extends UsernamePasswordToken {
public JWTToken() {
super();
}
public JWTToken(String token) {
super(token,token, false, null);
}
}
最后是refreshToken接口
@GetMapping("refresh_token")
@ApiOperation("刷新token")
public AjaxResult refresh_token(String refreshToken){
try {
String userId;
try {
userId = JWT.decode(refreshToken).getAudience().get(0);
} catch (JWTDecodeException j) {
throw new ViAuthenticationException("解析refresh_token失败:"+refreshToken);
}
User user = userService.selectById(Long.valueOf(userId));
if (user == null) {
throw new ViAuthenticationException("用户不存在,请重新登录");
}
// jwt 验证 refresh_token
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(ViConstants.PREFIX_VI_REFRESH_TOKEN_SIGN+user.getOpenId())).build();
try {
jwtVerifier.verify(refreshToken);
} catch(JWTVerificationException e) {
throw new ViAuthenticationException("jwt校验失败");
}
if(!verifyRefreshToken(refreshToken,user)){
throw new ViAuthenticationException("refresh_token过期");
}
String token = getToken(user);
TokenResult result=new TokenResult();
result.setToken(token);
//新的token存入redis中
redisRepository.setExpire(ViConstants.PREFIX_VI_REDIS_USER_TOKEN+user.getId(),token,3600*24);
return AjaxResult.success(result);
} catch (Exception e) {
e.printStackTrace();
return AjaxResult.error(e.getMessage());
}
}
boolean verifyRefreshToken(String refresh_token,User user){
String cacheToken = redisRepository.get(ViConstants.PREFIX_VI_REDIS_USER_REFRESH_TOKEN+ user.getId());
//当传入token与缓存中token相等时,说明token有效
if(!StringUtils.isEmpty(cacheToken)&&cacheToken.equals(refresh_token)){
return true;
}else {
return false;
}
}