认证授权-2
认证授权
├── 5 用户认证
│ ├── 5.1 认证流程
│ │ │
│ ├── 5.2 连接数据库认证
│ │ ├── (1) 原来的方法
│ │ ├── (2) 查询用户原理
│ │ ├── (3) 实现数据库认证
│ │ ├── (4) 修改加密方式
│ │ │
│ ├── 5.3 扩展用户信息
│ │ ├── (1) 目前问题
│ │ ├── (2) 方案一
│ │ ├── (3) 方案二
│ │ │
│ ├── 5.4 资源服务获取用户信息
│ │ ├── (1) 工具类
│ │ ├── (2) 获取用户信息
│ │ │
│ ├── 5.5 统一认证入口
│ │ ├── (1) 统一认证参数
│ │ ├── (2) 修改自定义UserDetailService
│ │ ├── (3) 自定义DaoAuthenticationProvider
│ │ ├── (4) 修改WebSecurityConfig
│ │ ├── (5) 统一认证接口--策略模式
│ │ ├── (6) 修改自定义UserDetailService
│ │ │
│ ├── 5.6 账号密码认证
├── 6 微信扫码
│ ├── 6.1 接入流程
│ │ │
│ ├── 6.2 接入分析
│ │ │
│ ├── 6.3 接口定义
│ │ ├── (1) 修改nginx.conf
│ │ ├── (2) 修改回调uri
│ │ ├── (3) RestTemplate远程调用
│ │ ├── (4) WxLogin
│ │ ├── (3) WxAuthService
│ │ ├── (4) excute
├── 7 微信扫码公众号登录
│ ├── 7.1 前端配置
│ │ │
│ ├── 7.2 跨域配置
│ │ │
│ ├── 7.3 集成微信登录sdk
│ │ ├── (1) 引入依赖
│ │ ├── (2) 配置文件
│ │ │
│ ├── 7.4 生成带参二维码
│ │ ├── (1) controller
│ │ ├── (2) 微信登录sdk配置类
│ │ │
│ ├── 7.5 事件处理器
│ │ ├── (1) 处理器逻辑
│ │ ├── (2) 处理器
│ │ ├── (3) 配置处理器
│ │ │
│ ├── 7.6 服务认证准备
│ │ ├── (1) Controller
│ │ ├── (2) 内网穿透
│ │ ├── (3) 微信公众平台
│ │ ├── (4) 配置callBackUrl
│ │ │
│ ├── 7.7 扫码发送授权链接
│ │ ├── (1) ScanHandler
│ │ ├── (2) WxMsgService
│ │ ├── (3) Controller
│ │ │
5 用户认证
5.1 认证流程
- 自定义了
UserDetailService
,重写loadUserByUsername()
方法- 拓展了用户信息
- 自定义了
DaoAuthenticationProvider
,重写additionalAuthenticationChecks()
方法
5.2 连接数据库认证
(1) 原来的方法
-
WebSecurityConfig
中的UserDetailService()
方法用户信息存储在内存中//配置用户信息服务 @Bean public UserDetailsService userDetailsService() { //这里配置用户信息,这里暂时使用这种方式将用户存储在内存中 InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build()); manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build()); return manager; }
(2) 查询用户原理
-
UserDetailService
是一个接口,其中实现了loadUserByUsername()
方法public interface UserDetailsService { UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException; }
-
其中
UserDetails
是用户信息接口public interface UserDetails extends Serializable { Collection<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); }
(3) 实现数据库认证
-
通过自定义
UserDetaislService接口
来实现 -
重写
loadUserByUsername()方法
查询数据库得到用户信息返回UserDetails
类型的用户信息即可/** * @Description: 自定义UserDetailService **/ @Slf4j @Component public class UserServiceImpl implements UserDetailsService { @Autowired private XcUserMapper xcUserMapper; @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { //账号 String username = s; //1.根据username账号查询数据库 XcUser xcUser = xcUserMapper.selectOne( new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, username)); //2.查询到用户不存在---->返回null---->springSecurity框架会抛出异常:用户不存在 if (xcUser == null) { return null; } //3.用户存在,拿到正确的密码,封装成UserDetails对象,返回给SpringSecurity框架,由框架进行密码比对 String password = xcUser.getPassword(); //获取密码 String[] authorities = {"test"}; //权限 UserDetails userDetails =User.withUsername(username) .password(password) .authorities(authorities) .build(); return userDetails; } }
(4) 修改加密方式
使用Bcrypt加密
-
WebSecurityConfig
-
AuthorizationServer
修改客户端密钥
5.3 扩展用户信息
(1) 目前问题
-
目前重写的
UserDetailService
中的loadUserByUsername()
方法返回的
UserDetail
只包含了username,password等基本信息,不够用UserDetails userDetails =User.withUsername(username) //这个username只是包含普通的username用户名 .password(password) .authorities(authorities) .build(); return userDetails;
(2) 方案一
- 可以扩展
UserDetails
,使之包括更多的自定义属性(但需要修改较多代码)
(3) 方案二
-
扩展username字段的内容,比如存入json数据作为username的内容
此时
username
只是一个名字,实际上存储的是user相关信息 -
修改重写的
UserDetailService
中的loadUserByUsername()
方法//根据username账号查询数据库 XcUser xcUser = xcUserMapper.selectOne( new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, username)); String password = xcUser.getPassword(); //获取密码 String[] authorities = {"test"}; //权限 //令牌中不存放密码(为了安全) xcUser.setpassword(null); //将数据库查询到的用户信息转为JSON String userString = JSON.toJSONString(xcUser); UserDetails userDetails =User.withUsername(userString) //userString包含了用户的其他信息 .password(password) .authorities(authorities) .build(); return userDetails;
5.4 资源服务获取用户信息
封装工具类获取用户信息
(1) 工具类
/**
* @Description: 获取当前用户身份工具类
**/
@Slf4j
public class SecurityUtil {
public static XcUser getUser() {
try {
// 拿到当前用户信息
Object principalObj = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principalObj instanceof String) {
//取出用户身份信息
String principal = principalObj.toString();
//将json转成对象
XcUser user = JSON.parseObject(principal, XcUser.class);
return user;
}
} catch (Exception e) {
log.error("获取当前登录用户身份出错:{}", e.getMessage());
e.printStackTrace();
}
return null;
}
@Data
public static class XcUser implements Serializable {
private static final long serialVersionUID = 1L;
private String id;
private String username;
private String password;
private String salt;
private String name;
private String nickname;
private String wxUnionid;
private String companyId;
/**
* 头像
*/
private String userpic;
private String utype;
private LocalDateTime birthday;
private String sex;
private String email;
private String cellphone;
private String qq;
/**
* 用户状态
*/
private String status;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
}
(2) 获取用户信息
SecurityUtil.XcUser user = SecurityUtil.getUser();
5.5 统一认证入口
(1) 统一认证参数
所有认证类型通用的DTO
/**
* @description 认证用户请求参数
*/
@Data
public class AuthParamsDto {
private String username; //用户名
private String password; //域 用于扩展
private String cellphone;//手机号
private String checkcode;//验证码
private String checkcodekey;//验证码key
private String authType; // 认证的类型 password:用户名密码模式类型 sms:短信模式类型
private Map<String, Object> payload = new HashMap<>();//附加数据,作为扩展,不同认证类型可拥有不同的附加数据。如认证类型为短信时包含smsKey : sms:3d21042d054548b08477142bbca95cfa; 所有情况下都包含clientId
}
(2) 修改自定义UserDetailService
修改接收的参数为AutoParamDto(统一认证参数,所有认证类型通用的DTO)
(3) 自定义DaoAuthenticationProvider
重写
additionalAuthenticationChecks()
方法,使方法内容为空因为原来的方法里校验了密码,但我们统一了认证入口,不是所有的认证类型都需要校验密码
重写
setUserDetailsService()
方法,指定我们自定义的UserDetailsService
/**
* @Description: 自定义DaoAuthenticationProvider
**/
@Component
@Slf4j
public class DaoAuthenticationProviderCustom extends DaoAuthenticationProvider {
/** UserDetailsService已经被重新定义
* 需要将自定义的UserDetailsService注入*/
@Autowired
public void setUserDetailsService(UserDetailsService userDetailsService){
//调用父类的注入方法,将自定义的UserDetailsService注入
super.setUserDetailsService(userDetailsService);
}
/** 重写方法,屏蔽密码对比 */
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
//重写方法内容为空
}
}
(4) 修改WebSecurityConfig
因为我们重写了
DaoAuthenticationProvider
,需要告诉springSecurity框架我们自定义的DaoAuthenticationProvider是哪个
@Autowired
DaoAuthenticationProviderCustom daoAuthenticationProviderCustom;
/** 告诉框架我们自定义的DaoAuthenticationProvider是哪个*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(daoAuthenticationProviderCustom);
}
(5) 统一认证接口--策略模式
/**
* @Description: 统一认证接口
**/
public interface AuthService {
/**
* 认证方法
* @param authParamsDto 认证参数
*/
XcUserExt execute(AuthParamsDto authParamsDto);
}
/**
* @Description: 微信认证
**/
@Service("wx_authservice") //定义beanName--------------------------
public class WxAuthServiceImpl implements AuthService {
@Override
public XcUserExt execute(AuthParamsDto authParamsDto) {
return null;
}
}
/**
* @Description: 账号密码认证
**/
@Service("password_authservice") //定义beanName--------------------------
public class PasswordAuthServiceImpl implements AuthService {
@Override
public XcUserExt execute(AuthParamsDto authParamsDto) {
return null;
}
}
(6) 修改自定义UserDetaillService
@Autowired
private ApplicationContext applicationContext;
@Override //传入的参数是AuthParamsDto的json字符串
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
AuthParamsDto authParamsDto = null;
try{
//转成AuthParamsDto对象
authParamsDto = JSON.parseObject(s, AuthParamsDto.class);
}catch (Exception e){
log.info("认证请求不符合项目要求:{}",s);
throw new RuntimeException("认证请求数据格式不对");
}
//修改---------------------------------------------------------------------
//获取认证类型
String authType = authParamsDto.getAuthType();
//根据认证类型从spring容器中取出指定的bean (通过bean的名字取出不同认证类型对应的bean)
String beanName = authType+"_authservice";
AuthService bean = applicationContext.getBean(beanName, AuthService.class);
//调用统一的excute方法 (不同认证类型对应不同的excute方法)
XcUserExt xcUserExt = bean.execute(authParamsDto);
//修改---------------------------------------------------------------------
//封装xcUSerExt用户信息为userDetails
UserDetails userDetails = getUserPrincipal(xcUserExt);
return userDetails;
}
/**
* 查询用户信息 ----------------------------------提取方法
*/
public UserDetails getUserPrincipal(XcUserExt user){
String password = user.getPassword();
//权限
String[] authorities = {"test"};
//根据用户id查询用户的权限
List<XcMenu> xcMenus = xcMenuMapper.selectPermissionByUserId(user.getId());
if(xcMenus.size()>0){
List<String> permissions= new ArrayList<>();
xcMenus.forEach(m->{
//拿到了用户拥有的权限标识符
permissions.add(m.getCode());
});
//将permissions转成数组
authorities = permissions.toArray(new String[0]);
}
//令牌中不存放密码(为了安全)
user.setPassword(null);
//将user对象转成json
String userString = JSON.toJSONString(user);
UserDetails userDetails = User.withUsername(userString)
.password(password)
.authorities(authorities)
.build();
return userDetails;
}
// ----------------------------------提取方法
5.6 账号密码认证
PasswordAuthServiceImpl
实现AuthService
接口,重写execute()
方法
/**
* @Description: 账号密码认证
**/
@Service("password_authservice")
public class PasswordAuthServiceImpl implements AuthService {
@Autowired
private XcUserMapper xcUserMapper;
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
CheckCodeClient checkCodeClient;
@Override
public XcUserExt execute(AuthParamsDto authParamsDto) {
//输入的验证码
String checkcode = authParamsDto.getCheckcode();
//验证码的key
String checkcodekey = authParamsDto.getCheckcodekey();
if(StringUtils.isEmpty(checkcode) || StringUtils.isEmpty(checkcodekey)){
throw new RuntimeException("请输入验证码");
}
//远程调用验证码服务接口---->校验验证码
Boolean verify = checkCodeClient.verify(checkcodekey, checkcode);
if (verify == null || !verify) {
throw new RuntimeException("验证码输入错误");
}
//账号
String username = authParamsDto.getUsername();
//1.根据username账号查询数据库
XcUser xcUser = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, username));
//2.查询到用户不存在---->返回null---->springSecurity框架会抛出异常:用户不存在
if (xcUser == null) {
throw new RuntimeException("账号不存在");
}
//3.用户存在,拿到正确的密码
String passwordDB = xcUser.getPassword(); //获取密码
//获取用户输入的密码
String passwordForm = authParamsDto.getPassword();
//验证密码是否正确
boolean matches = passwordEncoder.matches(passwordForm, passwordDB);
if (!matches) {
throw new RuntimeException("账号或密码错误");
}
XcUserExt xcUserExt = new XcUserExt();
BeanUtils.copyProperties(xcUser, xcUserExt);
return xcUserExt;
}
}
6 微信扫码
6.1 接入流程
接口文档:https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html
6.2 接入分析
- 需要定义接口接收微信下发的授权码
- 收到授权码调用微信接口申请令牌
- 携带令牌调用微信接口获取用户信息
- 获取用户信息将其写入数据库
- 重定向到浏览器自动登录
6.3 接口定义
(1) 修改nginx.conf
#微信扫码回调,监听8888端口(与redirect_uri对应)
server {
listen 8888;
server_name localhost;
location /api {
proxy_pass http://gatewayserver;
#proxy_pass http://localhost:64410;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 这里需要添加一个rewrite规则,把请求中的/api去掉
rewrite ^/api(.*)$ $1 break;
}
}
(2) 修改回调uri
//请用微信生成二维码
function generateWxQrcode(token) {
var wxObj = new WxLogin({
self_redirect:true,
id:"login_container",
appid: "wxed9954c01bb89b47 ",
scope: "snsapi_login",
//redirect_uri: "http://tjxt-user-t.itheima.net/xuecheng/auth/wxLogin", //修改前
redirect_uri: "http://localhost:8888/api/auth/wxLogin", //修改后
state: token,
style: "",
href: ""
});
}
(3) RestTemplate远程调用
使用
RestTemplate
调用第三方服务
//配置在启动类中
@Bean
RestTemplate restTemplate(){
RestTemplate restTemplate = new RestTemplate(new OkHttp3ClientHttpRequestFactory());
return restTemplate;
}
(4) WxLogin
用户扫码完成后,调用/auth/wxLogin接口(生成二维码时配置的redirect_uri)
/**
* @Description: 微信扫码接口
**/
@Slf4j
@Controller
public class WxLoginController {
@Autowired
WxAuthService wxAuthService;
@RequestMapping("/wxLogin")
public String wxLogin(String code, String state) throws IOException {
log.debug("微信扫码回调,code:{},state:{}",code,state);
//1.远程微信申请令牌,2.拿到令牌查询用户信息,3.将用户信息写入本项目数据库
XcUser xcUser = wxAuthService.wxAuth(code); //调用==================
if(xcUser==null){
return "redirect:http://www.51xuecheng.cn/error.html";
}
String username = xcUser.getUsername();
//重定向后 认证类型为wx的认证流程-->wxAuthService的excute()方法
return "redirect:http://www.51xuecheng.cn/sign.html?username="+username+"&authType=wx";
}
}
(5) WxAuthService
进入/wxLogin接口后
要执行 1.远程微信申请令牌,2.拿到令牌查询用户信息,3.将用户信息写入数据库
-
wxAuth()方法
@Override public XcUser wxAuth(String code) { //1.申请令牌 Map<String, String> accessTokenMap = getAccess_token(code); String access_token = accessTokenMap.get("access_token"); String openid = accessTokenMap.get("openid"); //2.携带令牌查询用户信息 Map<String, String> userinfo_map = getUserinfo(access_token, openid); //3.保存用户信息到数据库 //注意: 将自己注入,作为代理对象,调用事务方法,事务注解才生效 XcUser xcUser = currentProxy.addXcUser(userinfo_map); return xcUser; }
-
getAccess_token()
携带授权码申请令牌/** * 1.携带授权码申请令牌 * POST "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code" * * 结果为JSON格式---->使用Map * { * "access_token":"ACCESS_TOKEN", * "expires_in":7200, * "refresh_token":"REFRESH_TOKEN", * "openid":"OPENID", * "scope":"SCOPE", * "unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL" * } * * @param code 授权码 * @return */ private Map<String,String> getAccess_token(String code){ String url_temple = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code"; //最终的请求路径 String url = String.format(url_temple, appid, secret, code); //远程调用url---->使用RestTemplate ResponseEntity<String> exchange = restTemplate.exchange(url, HttpMethod.POST, null, String.class); //获取响应的结果 String result = exchange.getBody(); //将result转成map Map<String,String> map = JSON.parseObject(result, Map.class); return map; }
-
getUserinfo()
携带令牌查询用户信息/** * 2.携带令牌查询用户信息 * GET "https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID" * * 响应结果为JSON---->map * { * "openid":"OPENID", * "nickname":"NICKNAME", * "sex":1, * "province":"PROVINCE", * "city":"CITY", * "country":"COUNTRY", * "headimgurl": "https://thirdwx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/0", * "privilege":[ * "PRIVILEGE1", * "PRIVILEGE2" * ], * "unionid": " o6_bmasdasdsad6_2sgVt7hMZOPfL" * } * * @param access_token * @param openid * @return */ private Map<String,String> getUserinfo(String access_token,String openid){ String url_template= "https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s"; String url = String.format(url_template, access_token, openid); //远程调用 ResponseEntity<String> exchange = restTemplate.exchange(url, HttpMethod.GET, null, String.class); //获取响应的结果---->乱码:转成UTF-8 String result = new String(exchange.getBody().getBytes(StandardCharsets.ISO_8859_1),StandardCharsets.UTF_8); //将result转成map Map<String,String> map = JSON.parseObject(result, Map.class); return map; }
-
addXcUser()
保存用户信息到数据库/** * 3.保存用户信息到数据库 * @param userInfo_map * @return */ @Transactional public XcUser addXcUser(Map<String,String> userInfo_map){ String unionid = userInfo_map.get("unionid"); String nickname = userInfo_map.get("nickname"); //通过unionid查询用户信息 XcUser xcUserDB = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getWxUnionid, unionid)); if(xcUserDB != null ){ return xcUserDB; } //向数据库新增记录 XcUser xcUser = new XcUser(); String userId = UUID.randomUUID().toString(); xcUser.setId(userId); //主键 xcUser.setUsername(unionid); xcUser.setPassword(unionid); xcUser.setWxUnionid(unionid); xcUser.setNickname(nickname); xcUser.setName(nickname); xcUser.setUtype("101001"); //学生类型 xcUser.setStatus("1"); //用户状态 xcUser.setCreateTime(LocalDateTime.now()); //插入 xcUserMapper.insert(xcUser); //向用户角色关系表新增记录 XcUserRole xcUserRole = new XcUserRole(); xcUserRole.setId(UUID.randomUUID().toString()); xcUserRole.setUserId(userId); xcUserRole.setRoleId("17"); //学生角色 xcUserRole.setCreateTime(LocalDateTime.now()); xcUserRoleMapper.insert(xcUserRole); return xcUser; }
(6) excute
扫码后调用/wxLogin接口,执行完 (1.远程微信申请令牌,2.拿到令牌查询用户信息,3.将用户信息写入本项目数据库) 后
重定向到http://www.51xuecheng.cn/sign.html?username="+username+"&authType=wx
(此处的username实际上是统一认证参数AuthParamDto的json串)
(执行认证类型为wx的认证流程-->
wxAuthService
的excute()
方法)
@Override
public XcUserExt execute(AuthParamsDto authParamsDto) {
//得到账号
String username = authParamsDto.getUsername();
//查询数据库
XcUser xcUser = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, username));
if(xcUser == null){
throw new RuntimeException("用户不存在");
}
XcUserExt xcUserExt = new XcUserExt();
BeanUtils.copyProperties(xcUser,xcUserExt);
return xcUserExt;
}
7 微信扫码公众号登录
7.1 前端配置
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>微信登录二维码</title>
</head>
<body>
<div class="container">
<img src="../img/wx-icon.png" alt="微信登录" class="wechat-icon" onclick="generateQRCode()">
<div id="qrCodeContainer">
<h3>扫描二维码登录</h3>
<img id="qrCode" alt="二维码">
</div>
</div>
<script>
function generateQRCode() {
// 获取二维码容器
const qrCodeContainer = document.getElementById("qrCodeContainer");
const qrCodeImg = document.getElementById("qrCode");
// 显示二维码容器
qrCodeContainer.style.display = "block";
// 通过后端接口获取二维码
fetch("http://127.0.0.1:8099/wx/public/gen", {
method: "GET", // 请求方式
credentials: "include" // 允许携带凭证(如 Cookies 或认证信息)
})
.then(response => response.text()) // 由于返回的是URL字符串,所以使用 text() 解析
.then(url => {
// 假设API返回的JSON数据中二维码的URL在data.qrCodeUrl中
if (url) {
qrCodeImg.src = url;
} else {
qrCodeImg.alt = "二维码数据不正确";
}
})
.catch(error => {
console.error("获取二维码失败:", error);
qrCodeImg.alt = "二维码加载失败";
});
}
</script>
</body>
</html>
<style>
/* 简单的页面样式 */
.container {
text-align: center;
margin-top: 50px;
}
/* 微信登录图标按钮 */
.wechat-icon {
width: 60px;
height: 60px;
cursor: pointer;
border-radius: 5px;
/* 圆形按钮 */
border: 2px solid #25d366;
/* 微信绿色 */
padding: 10px;
background-color: #f5f5f5;
transition: all 0.3s ease;
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2);
/* 阴影 */
}
/* 鼠标悬停效果 */
.wechat-icon:hover {
transform: scale(1.1);
/* 放大效果 */
background-color: #e0e0e0;
/* 变为微信绿色背景 */
box-shadow: 0px 6px 12px rgba(0, 0, 0, 0.3);
/* 增强阴影 */
}
#qrCodeContainer {
margin-top: 20px;
display: none;
/* 初始隐藏 */
}
#qrCode {
width: 200px;
height: 200px;
border: 1px solid #ddd;
}
</style>
7.2 跨域配置
/**
* @Author: ciky
* @Description: webMVC配置类
* @DateTime: 2024/11/13 11:48
**/
@Configuration
public class WebConfig implements WebMvcConfigurer {
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 允许所有路径
.allowedOrigins("http://127.0.0.1:5500") // 允许 localhost:5500 域名的请求
.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS") // 允许的请求方式
.allowCredentials(true) // 允许携带认证信息(如 Cookie)
.maxAge(3600)
.allowedHeaders("*"); // 允许所有请求头
}
}
7.3 集成微信登录sdk
(1) 引入依赖
<!--微信登录sdk-->
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
<version>4.4.0</version>
</dependency>
(2) 配置文件
wx:
mp:
callback: ${mallchat.wx.callback}
configs:
- appId: ${mallchat.wx.appId} # 第一个公众号的appid
secret: ${mallchat.wx.secret} # 公众号的appsecret
token: ${mallchat.wx.token} # 接口配置里的Token值
aesKey: ${mallchat.wx.aesKey} # 接口配置里的EncodingAESKey值(测试号不需要)
7.4 生成带参二维码
(1) Controller
/**
* @Author: ciky
* @Description: 微信登录api交互接口
* @DateTime: 2024/11/13 11:26
**/
@RestController
@RequestMapping("/wx/public")
public class WxLoginController {
private static final Logger LOG = LoggerFactory.getLogger(WxLoginController.class);
@Autowired
private WxMpService wxMpService;
@GetMapping("/gen")
public String getQrCode() throws WxErrorException {
// 获取临时二维码ticket
WxMpQrCodeTicket ticket = wxMpService.getQrcodeService().qrCodeCreateTmpTicket(1, 10000);
// 用ticket获取二维码图片url地址
String url = wxMpService.getQrcodeService().qrCodePictureUrl(ticket.getTicket());
LOG.info("申请的二维码:{}",url);
return url;
}
}
(2) 微信登录sdk配置类
/**
* @Author: ciky
* @Description: 微信登录sdk配置类
* @DateTime: 2024/11/13 11:33
**/
@Configuration
public class WxMpConfig {
@Value("${wx.mp.appId}")
private String appId;
@Value("${wx.mp.secret}")
private String secret;
@Value("${wx.mp.token}")
private String token;
@Value("${wx.mp.aesKey}")
private String aesKey;
@Bean
public WxMpService wxMpService() {
//创建一个WxMpServiceImpl实例(该类实现了 WxMpService 接口,负责与微信公众平台的交互)
WxMpService service = new WxMpServiceImpl();
WxMpDefaultConfigImpl configStore = new WxMpDefaultConfigImpl();
configStore.setAppId(appId);
configStore.setSecret(secret);
configStore.setToken(token);
configStore.setAesKey(aesKey);
service.setWxMpConfigStorage(configStore);
return service;
}
}
7.5 事件处理器
(1) 处理器逻辑
AbstractHandler
实现WxMpMessageHandler
接口,ScanHandler
继承AbstractHandler
SubscribeHandler
继承AbstractHandler
(2) 处理器
/**
* @Description: 处理器抽象类
**/
public abstract class AbstractHandler implements WxMpMessageHandler {
}
/**
* @Description: 扫码事件处理器
**/
@Component
public class ScanHandler extends AbstractHandler {
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMpXmlMessage, Map<String, Object> map, WxMpService wxMpService, WxSessionManager wxSessionManager) throws WxErrorException {
return null;
}
}
/**
* @Description: 新关注事件处理器
**/
@Component
public class SubscribeHandler extends AbstractHandler{
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMpXmlMessage, Map<String, Object> map, WxMpService wxMpService, WxSessionManager wxSessionManager) throws WxErrorException {
return null;
}
}
(3) 配置处理器
/**
* @Description: 微信登录sdk配置类
**/
@Configuration
public class WxMpConfig {
@Autowired
private ScanHandler scanHandler; //扫码事件处理器
@Autowired
private SubscribeHandler subscribeHandler; //扫码事件处理器
@Bean
public WxMpMessageRouter messageRouter(WxMpService wxMpService) {
final WxMpMessageRouter newRouter = new WxMpMessageRouter(wxMpService);
// 关注事件
newRouter.rule().async(false).msgType(EVENT).event(SUBSCRIBE).handler(this.subscribeHandler).end();
// 扫码事件
newRouter.rule().async(false).msgType(EVENT).event(WxConsts.EventType.SCAN).handler(this.scanHandler).end();
return newRouter;
}
}
7.6 服务认证准备
(1) Controller
/**
* @Author: ciky
* @Description: 微信登录api交互接口
* @DateTime: 2024/11/13 11:26
**/
@RestController
@RequestMapping("/wx/public")
public class WxLoginController {
private static final Logger LOG = LoggerFactory.getLogger(WxLoginController.class);
@Autowired
private WxMpService wxMpService;
@Autowired
private WxMpMessageRouter wxMpMessageRouter;
@GetMapping(produces = "text/plain;charset=utf-8")
public String authGet(@RequestParam(name = "signature", required = false) String signature,
@RequestParam(name = "timestamp", required = false) String timestamp,
@RequestParam(name = "nonce", required = false) String nonce,
@RequestParam(name = "echostr", required = false) String echostr) {
LOG.info("\n接收到来自微信服务器的认证消息:[{}, {}, {}, {}]", signature,
timestamp, nonce, echostr);
if (StringUtils.isAnyBlank(signature, timestamp, nonce, echostr)) {
throw new IllegalArgumentException("请求参数非法,请核实!");
}
if (wxMpService.checkSignature(timestamp, nonce, signature)) {
return echostr;
}
return "非法请求";
}
@PostMapping(produces = "application/xml; charset=UTF-8")
public String post(@RequestBody String requestBody,
@RequestParam("signature") String signature,
@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce,
@RequestParam("openid") String openid,
@RequestParam(name = "encrypt_type", required = false) String encType,
@RequestParam(name = "msg_signature", required = false) String msgSignature) {
LOG.info("\n接收微信请求:[openid=[{}], [signature=[{}], encType=[{}], msgSignature=[{}],"
+ " timestamp=[{}], nonce=[{}], requestBody=[\n{}\n] ",
openid, signature, encType, msgSignature, timestamp, nonce, requestBody);
if (!wxMpService.checkSignature(timestamp, nonce, signature)) {
throw new IllegalArgumentException("非法请求,可能属于伪造的请求!");
}
String out = null;
if (encType == null) {
// 明文传输的消息
WxMpXmlMessage inMessage = WxMpXmlMessage.fromXml(requestBody);
WxMpXmlOutMessage outMessage = this.route(inMessage);
if (outMessage == null) {
return "";
}
out = outMessage.toXml();
} else if ("aes".equalsIgnoreCase(encType)) {
// aes加密的消息
WxMpXmlMessage inMessage = WxMpXmlMessage.fromEncryptedXml(requestBody, wxMpService.getWxMpConfigStorage(),
timestamp, nonce, msgSignature);
LOG.debug("\n消息解密后内容为:\n{} ", inMessage.toString());
WxMpXmlOutMessage outMessage = this.route(inMessage);
if (outMessage == null) {
return "";
}
out = outMessage.toEncryptedXml(wxMpService.getWxMpConfigStorage());
}
LOG.debug("\n组装回复信息:{}", out);
return out;
}
private WxMpXmlOutMessage route(WxMpXmlMessage message) {
try {
return this.wxMpMessageRouter.route(message);
} catch (Exception e) {
LOG.error("路由消息时出现异常!", e);
}
return null;
}
}
(2) 内网穿透
cpolar http 8099
(3) 微信公众平台
(4) 配置callBackUrl
wx:
mp:
callback: http://42d6c9c2.r22.cpolar.top
7.7 扫码发送授权链接
(1) ScanHandler
扫码后监听到扫码事件,进行处理
/**
* @Description: 扫码事件处理器
**/
@Component
public class ScanHandler extends AbstractHandler {
@Autowired
@Lazy
private WxMsgService wxMsgService;
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMpXmlMessage, Map<String, Object> map, WxMpService wxMpService, WxSessionManager wxSessionManager) throws WxErrorException {
return wxMsgService.scan(wxMpXmlMessage);
}
}
(2) WxMsgService
扫码事件的处理逻辑
- 授权接口:https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
- 回调地址:http://42d6c9c2.r22.cpolar.top/wx/public/callBack (自定义)
/**
* @Description: 微信事件处理逻辑实现类
**/
@Service
public class WxMsgServiceImpl implements WxMsgService {
private static final Logger LOG = LoggerFactory.getLogger(WxMsgServiceImpl.class);
@Value("${wx.mp.callback}")
private String callback;
@Autowired
private WxMpService wxMpService;
/**
* 授权接口url
*/
public static final String URL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect";
public WxMpXmlOutMessage scan(WxMpXmlMessage wxMpXmlMessage) {
LOG.info("---扫码事件开始---");
// 获取openId
String openId = wxMpXmlMessage.getFromUser();
// 获取授权码code
Integer code = getEventKey(wxMpXmlMessage);
LOG.info("openId:{},code:{}",openId,code);
if(Objects.isNull(code)){
return null;
}
/**
* 授权接口:
* https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
*/
String authorizeUrl = String.format(URL, wxMpService.getWxMpConfigStorage().getAppId(), URLEncoder.encode(callback+"/wx/public/callBack"));
// 向用户返回授权链接
return WxMpXmlOutMessage.TEXT().content("请点击链接授权:<a href=\""+authorizeUrl+"\"> 登录 </a>")
.fromUser(wxMpXmlMessage.getToUser())
.toUser(wxMpXmlMessage.getFromUser())
.build();
}
/**
* 获取授权码code
*/
private Integer getEventKey(WxMpXmlMessage wxMpXmlMessage) {
try {
//事件码:qrscene_2 --> 新关注
String eventKey = wxMpXmlMessage.getEventKey();
String code = eventKey.replace("qrscene_", "");
return Integer.parseInt(code);
}catch (Exception e){
LOG.error("getEventKey error, eventKey:{}",wxMpXmlMessage.getEventKey());
return null;
}
}
}
(3) Controller
@GetMapping("/callBack")
public RedirectView callBack(@RequestParam String code) throws WxErrorException {
/**
* 用户点击公众号发的"授权"连接后,回调此方法
* 1. 通过code换取access_token
* 2. 通过access_token获取用户信息
*/
WxOAuth2AccessToken accessToken = wxMpService.getOAuth2Service().getAccessToken(code);
WxOAuth2UserInfo userInfo = wxMpService.getOAuth2Service().getUserInfo(accessToken, "zh_cCN");
LOG.info("获取到的用户信息:{}",userInfo);
RedirectView redirectView = new RedirectView();
redirectView.setUrl("https://www.ciky.cloud");
return redirectView;
}
获取到的用户信息:
WxOAuth2UserInfo(openid=oOQUs6ghkB4RMZes4JfdgW3D1PR8, nickname=Ciky, sex=0, city=, province=, country=, headImgUrl=https://thirdwx.qlogo.cn/mmopen/vi_32/PiajxSqBRaELbLvJlx2ibZZHKIHZS7e169ibL1W8EeueM8LZ4dpAtMgC6UCNHVW9Mf0ZM4Tgw9tRbDH0TNmezVKfrl4zdrhGoBic1oYVz2XxmxvYPIaugDd7ww/132,
unionId=null, privileges=[], snapshotUser=null)