首先,我们先看一下,微信官方推荐的登录流程,后序的开发就是按这个来的。
仔细看这张图会发现,我们的难处有两点:
先简述下原理:
http 协议是无状态协议。我们并不能通过每次的请求知晓操作者是谁。为了解决这个问题,提出了 cookie 的概念,解决这个问题。
客户与服务器之间通过 sessionId 进行交流。
当然这也是 shiro 的原理。shiro 就是通过 request header 中的 JSessionId 识别用户的。
在调用我们的服务器根据用户的 openid 确认到给用户后,shiro 此时也完成了用户的登录,并把用户信息保存起来了,最后,controller 返回处理结果。(具体代码见下方)
我们在微信小程序端要获取到 JSeesionId ,并保存:
(下方代码在 app.js 的 onLounch() 方法里)
let JSessionId = response.header["Set-Cookie"].toString().split(';')[0].substring(11);
wx.setStorageSync("JSessionId", JSessionId);
在需要用户登录的地方,我们只要注意两点:
wx.request()
向有登录要求的 URL 发送请求时,在 header 中带上该字段,代码如下:wx.request({
url: URL,
header: {
"Content-Type": "application/x-www-form-urlencoded",
"JSessionId": wx.getStorageSync('JSessionId')
},
data: {},
success(response) {}
)};
sessionManager
,并将其注入到 shiro 的 securityManager
sessionManager
public class WeChatSessionManager extends DefaultWebSessionManager {
public final static String HEADER_TOKEN_NAME = "JSessionId";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
/**
* 逻辑:
* 如果请求头中有 JSessionId,就分析它;
* 没有就调用父类的方法
*/
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response){
String JSessionId = WebUtils.toHttp(request).getHeader(HEADER_TOKEN_NAME);
if(JSessionId == null) {
return super.getSessionId(request, response);
} else {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, JSessionId);
log.info("JSessionId: {}", JSessionId);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return JSessionId;
}
}
}
sessionManager
注入到 shiro 的 securityManager
sessionManager
做些自己的定义@Configuration
public class ShiroConfig {
/**
* 其它代码
* /
@Bean
public WeChatSessionManager sessionManager() {
WeChatSessionManager weChatSessionManager = new WeChatSessionManager();
return weChatSessionManager;
}
@Bean
public SecurityManager securityManager(@Qualifier("realm") Realm realm,
@Qualifier("sessionManager") SessionManager sessionManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
securityManager.setSessionManager(sessionManager);
return securityManager;
}
/**
* 其它代码
* /
}
//app.js
App({
globalData: {
timeout: 30000,
localhost: "http://localhost:8080/miniprogram",
login: false,
},
/**
* 小程序初始化
*/
onLaunch: function () {
let p = this;
wx.login({
success(res) {
if (res.code) {
p.login(res.code);
}
},
fail: () => wx.showModal({
content: "获取 code 失败",
showCancel: false
})
});
},
login(code) {
let p = this;
wx.request({
url: p.globalData.localhost + "/noLogin/login",
method: "POST",
header: {"Content-Type": "application/x-www-form-urlencoded"},
data: {code: code},
success(response) {
switch(response.data["data"]) {
case "unregistered":
wx.showModal({
content: "您未注册,是否前往注册"
});
break;
case "registered":
p.globalData.login = true;
let JSessionId = response.header["Set-Cookie"].toString().split(';')[0].substring(11);
wx.setStorageSync("JSessionId", JSessionId);
break;
}
},
});
}
})
主要是 shiro 的 dependency
,其它请根据自己的业务需求自行添加
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-springartifactId>
<version>1.7.0version>
dependency>
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import top.leeti.entity.User;
import top.leeti.service.UserService;
import top.leeti.util.PasswordUtil;
import javax.annotation.Resource;
@Slf4j
public class MyRealm extends AuthorizingRealm {
@Resource
private UserService userService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
throws AuthenticationException {
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
User user = userService.getUserByOpenId(usernamePasswordToken.getUsername());
if (user == null) {
throw new AccountException();
}
ByteSource saltOfCredential = ByteSource.Util.bytes(user.getStuId());
return new SimpleAuthenticationInfo(user, String.valueOf(usernamePasswordToken.getPassword()),
saltOfCredential, getName());
}
}
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;
/**
* 自定义session管理器
* 继承DefaultWebSessionManager,重写getSessionId方法
*/
@Slf4j
public class WeChatSessionManager extends DefaultWebSessionManager {
public final static String HEADER_TOKEN_NAME = "JSessionId";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
/**
* 逻辑:
* 如果请求头中有 JSessionId,就分析它;
* 没有就调用父类的方法
*/
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response){
String JSessionId = WebUtils.toHttp(request).getHeader(HEADER_TOKEN_NAME);
if(JSessionId == null) {
return super.getSessionId(request, response);
} else {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, JSessionId);
log.info("JSessionId: {}", JSessionId);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return JSessionId;
}
}
}
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import top.leeti.config.manager.WeChatSessionManager;
import top.leeti.realm.MyRealm;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
@Bean
public CredentialsMatcher credentialsMatcher() {
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
matcher.setHashAlgorithmName("MD5");
matcher.setHashIterations(1024);
return matcher;
}
@Bean
public Realm realm(@Qualifier("credentialsMatcher") CredentialsMatcher credentialsMatcher) {
MyRealm realm = new MyRealm();
realm.setCredentialsMatcher(credentialsMatcher);
return new MyRealm();
}
@Bean
public WeChatSessionManager sessionManager() {
WeChatSessionManager weChatSessionManager = new WeChatSessionManager();
return weChatSessionManager;
}
@Bean
public SecurityManager securityManager(@Qualifier("realm") Realm realm,
@Qualifier("sessionManager") SessionManager sessionManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
securityManager.setSessionManager(sessionManager);
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 设置默认登录的 url(若登录失败,则转到此)
shiroFilterFactoryBean.setLoginUrl("/app/noLogin/error");
// 设置登录认证成功后默认转到的 url
// shiroFilterFactoryBean.setSuccessUrl("/admin/index");
// 设置权限认证失败时转到的 url
shiroFilterFactoryBean.setUnauthorizedUrl("/app/noLogin/noAccess");
/*
* anon:匿名用户可访问
* authc:认证用户可访问
* user:使用rememberMe可访问
* perms:对应权限可访问
* roles[角色名]:对应角色权限可访问
*/
//设置访问各 url 的权限
Map<String, String> filterChain = new HashMap<>(5);
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChain);
return shiroFilterFactoryBean;
}
}
package top.leeti.controller.nologin;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.*;
import top.leeti.entity.result.Result;
import top.leeti.myenum.ResultCodeEnum;
import top.leeti.util.WechatUtil;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/miniprogram/noLogin/")
public class LoginController {
@PostMapping("login")
public String login(@RequestParam String code) {
Map<String, String> map = WechatUtil.acquireSessionKeyAndOpenId(code);
String openId = map.get("openId");
Result<String> result = null;
if (openId == null) {
result = new Result<>(null, "获取openId失败", null, false);
} else {
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(openId, "");
Subject currentUser = SecurityUtils.getSubject();
try{
currentUser.login(usernamePasswordToken);
result = new Result<>(null, null, "registered", true);
} catch(AccountException accountException) {
result = new Result<>(null, null, "unregistered", false);
return JSON.toJSONString(result);
}
}
return JSON.toJSONString(result);
}
}
package top.leeti.util;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import java.util.Hashtable;
import java.util.Map;
@Slf4j
public class WechatUtil {
private static final String APP_ID = "wxf4eeba633c89a51f";
private static final String APP_SECRET = "8a7de97a3189c29e3faf87275e3449ee";
private static final String GRANT_TYPE = "authorization_code";
/**
* 像微信服务器发送请求,获取 sessionKey、openId
* @param code
* 本次登录请求的小程序传来的标识(保证传来的 code 绝不为空或是空字符串)
* @return
* Map key:sessionKey、openId。如果不能正确地获取到 sessionKey、openId,
* 返回的 map 中,值为 null。
*/
public static Map<String, String> acquireSessionKeyAndOpenId(String code){
RestTemplate restTemplate = new RestTemplate();
Map<String, String> map = new Hashtable<>();
try{
String url = String.format("https://api.weixin.qq.com/sns/jscode2session" +
"?appid=%s&secret=%s&js_code=%s&grant_type=%s", APP_ID, APP_SECRET, code, GRANT_TYPE);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
ResponseEntity<String> response = restTemplate.getForEntity( url, String.class);
String[] results = response.getBody().split("\"");
map.put("sessionKey", results[4]);
map.put("openId", results[7]);
} catch (Exception e){
map.put("sessionKey", null);
map.put("openId", null);
}
return map;
}
}
我这里是把 openid 保存到了数据库。
因为每个小程序的中每个用户的 openid 具有唯一性,我就把它作为判断用户是否注册的凭证