SpringBoot 集成 微信登录
重写一个认证逻辑 实现AuthenticationProvider
import com.hzjtcl.commons.security.service.HzjtclUserDetailsService;
import lombok.Getter;
import lombok.Setter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.userdetails.UserDetails;
/**
* @author gczjt
* @date 2018/8/5 手机登录校验逻辑 验证码登录、社交登录
*/
@Slf4j
public class MobileAuthenticationProvider implements AuthenticationProvider {
private MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
@Getter
@Setter
private HzjtclUserDetailsService userDetailsService;
@Override
@SneakyThrows
public Authentication authenticate(Authentication authentication) {
MobileAuthenticationToken mobileAuthenticationToken = (MobileAuthenticationToken) authentication;
String principal = mobileAuthenticationToken.getPrincipal().toString();
// 获取用户信息时进行微信登录认证
UserDetails userDetails = userDetailsService.loadUserBySocial(principal);
if (userDetails == null) {
log.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages
.getMessage("AbstractUserDetailsAuthenticationProvider.noopBindAccount", "Noop Bind Account"));
}
// 检查账号状态
//detailsChecker.check(userDetails);
MobileAuthenticationToken authenticationToken = new MobileAuthenticationToken(userDetails,
userDetails.getAuthorities());
authenticationToken.setDetails(mobileAuthenticationToken.getDetails());
return authenticationToken;
}
@Override
public boolean supports(Class<?> authentication) {
return MobileAuthenticationToken.class.isAssignableFrom(authentication);
}
}
import lombok.SneakyThrows;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import java.util.Collection;
/**
* @author gczjt
* @date 2018/1/9 手机号登录令牌
*/
public class MobileAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal;
public MobileAuthenticationToken(String mobile) {
super(null);
this.principal = mobile;
setAuthenticated(false);
}
public MobileAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true);
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public Object getCredentials() {
return null;
}
@Override
@SneakyThrows
public void setAuthenticated(boolean isAuthenticated) {
if (isAuthenticated) {
throw new IllegalArgumentException(
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}
登录认证及获取用户信息
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
/**
* @author gczjt
* @date 2018/8/15
*/
public interface HzjtclUserDetailsService extends UserDetailsService {
/**
* 根据社交登录code 登录
* @param code TYPE@CODE
* @return UserDetails
* @throws UsernameNotFoundException
*/
UserDetails loadUserBySocial(String social) throws UsernameNotFoundException;
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException ;
}
import com.hzjtcl.commons.security.enums.UserStatusEnum;
import com.hzjtcl.commons.security.feign.AccountFeignClient;
import com.hzjtcl.commons.security.service.HzjtclUserDetailsService;
import com.hzjtcl.commons.security.user.UserDetail;
import com.hzjtcl.commons.tools.exception.ErrorCode;
import com.hzjtcl.commons.tools.exception.RenException;
import com.hzjtcl.commons.tools.utils.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
/**
* UserDetailsService
*
* @author Mark sunlightcs@gmail.com
*/
@Service
public class RenUserDetailsServiceImpl implements HzjtclUserDetailsService {
@Autowired(required=false)
private AccountFeignClient accountFeignClient;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Result<UserDetail> result = accountFeignClient.getByUsername(username);
UserDetail userDetail = result.getData();
//账号不存在
if(userDetail == null){
throw new RenException(ErrorCode.ACCOUNT_NOT_EXIST);
}
//账号不可用
if(userDetail.getStatus() == UserStatusEnum.DISABLE.value()){
throw new RenException(ErrorCode.ACCOUNT_DISABLE);
}
return userDetail;
}
@Override
public UserDetails loadUserBySocial(String social) throws UsernameNotFoundException {
Result<UserDetail> result = accountFeignClient.loadUserBySocial(social);
UserDetail userDetail = result.getData();
//账号不存在
if(userDetail == null){
return null;
}
//账号不可用
if(userDetail.getStatus() == UserStatusEnum.DISABLE.value()){
return null;
}
return userDetail;
}
}
import com.hzjtcl.commons.security.feign.fallback.AccountFeignClientFallbackFactory;
import com.hzjtcl.commons.security.user.UserDetail;
import com.hzjtcl.commons.tools.constant.ServiceConstant;
import com.hzjtcl.commons.tools.utils.Result;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
/**
* 账号接口
*
* @author Mark sunlightcs@gmail.com
*/
@FeignClient(name = ServiceConstant.HZJTCL_ADMIN_SERVER, contextId = "AccountFeignClient", fallbackFactory = AccountFeignClientFallbackFactory.class)
public interface AccountFeignClient {
/**
* 根据用户名,获取用户信息
* @param username 用户名
*/
@PostMapping("sys/user/getByUsername")
Result<UserDetail> getByUsername(@RequestParam("username") String username);
/**
* 通过社交账号或手机号查询用户、角色信息
* @param inStr appid@code
*/
@PostMapping("sys/user/socialinfo")
Result<UserDetail> loadUserBySocial(@RequestParam("inStr") String inStr);
}
controller 层自己去实现
service用户信息认证
@Autowired
private Map<String, LoginHandler> loginHandlerMap;
@Override
public SysUserDTO getSocialinfoByinStr(String inStr) {
String[] inStrs = inStr.split(StringPool.AT);
String type = inStrs[0];
String loginStr = inStrs[1];
// 通过前端传递来的参数在map中获取 type为解析出的类的名称 通过@Component注解获取对应类
return loginHandlerMap.get(type).handle(loginStr);
}
handler 处理类
/**
* 登录处理器
*/
public interface LoginHandler {
/***
* 数据合法性校验
* @param loginStr 通过用户传入获取唯一标识
* @return
*/
Boolean check(String loginStr);
/**
* 通过用户传入获取唯一标识
* @param loginStr
* @return
*/
String identify(String loginStr);
/**
* 通过openId 获取用户信息
* @param identify
* @return
*/
SysUserDTO info(String identify);
/**
* 处理方法
* @param loginStr 登录参数
* @return
*/
SysUserDTO handle(String loginStr);
}
public abstract class AbstractLoginHandler implements LoginHandler {
/***
* 数据合法性校验
* @param loginStr 通过用户传入获取唯一标识
* @return 默认不校验
*/
@Override
public Boolean check(String loginStr) {
return true;
}
/**
* 处理方法
* @param loginStr 登录参数
* @return
*/
@Override
public SysUserDTO handle(String loginStr) {
if (!check(loginStr)) {
return null;
}
String identify = identify(loginStr);
return info(identify);
}
}
mport com.hzjtcl.dto.SysUserDTO;
import com.hzjtcl.service.SysUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author gczjt
* @date 2018/11/18
*/
@Slf4j
@Component("SMS")
public class SmsLoginHandler extends AbstractLoginHandler {
@Autowired
private SysUserService sysUserService;
/**
* 验证码登录传入为手机号 不用不处理
* @param mobile
* @return
*/
@Override
public String identify(String mobile) {
return mobile;
}
/**
* 通过mobile 获取用户信息
* @param identify
* @return
*/
@Override
public SysUserDTO info(String identify) {
SysUserDTO user = sysUserService.getOneByinStr(identify);
if (user == null) {
log.info("手机号未注册:{}", identify);
return null;
}
return user;
}
}
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.hzjtcl.commons.security.constant.SecurityConstants;
import com.hzjtcl.commons.tools.utils.ConvertUtils;
import com.hzjtcl.dao.SysSocialDetailsDao;
import com.hzjtcl.dao.SysUserDao;
import com.hzjtcl.dto.SysUserDTO;
import com.hzjtcl.entity.SysSocialDetailsEntity;
import com.hzjtcl.entity.SysUserEntity;
import com.hzjtcl.enums.LoginTypeEnum;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component("WX") // 通过前端传递来的参数在map中获取
@AllArgsConstructor
public class WeChatLoginHandler extends AbstractLoginHandler {
private final SysUserDao sysUserDao;
private final SysSocialDetailsDao sysSocialDetailsDao;
/**
* 微信登录传入code
* <p>
* 通过code 调用qq 获取唯一标识
* @param code
* @return
*/
@Override
public String identify(String code) {
SysSocialDetailsEntity condition = new SysSocialDetailsEntity();
condition.setType(LoginTypeEnum.WECHAT.getType());
SysSocialDetailsEntity socialDetails = sysSocialDetailsDao.selectOne(new QueryWrapper<>(condition));
String url = String.format(SecurityConstants.WX_AUTHORIZATION_CODE_URL, socialDetails.getAppId(),
socialDetails.getAppSecret(), code);
String result = HttpUtil.get(url);
log.debug("微信响应报文:{}", result);
Object obj = JSONUtil.parseObj(result).get("openid");
if (obj == null) {
return "";
}
return obj.toString();
// return code;
}
/**
* openId 获取用户信息
* @param openId
* @return
*/
@Override
public SysUserDTO info(String openId) {
SysUserEntity user = sysUserDao.getOneByinStr(openId);
if (user == null) {
log.info("微信未绑定:{}", openId);
return null;
}
return ConvertUtils.sourceToTarget(user, SysUserDTO.class);
}
}
工具类
public interface SecurityConstants {
/**
* 编码
*/
String UTF8 = "UTF-8";
/**
* 成功标记
*/
Integer SUCCESS = 0;
/**
* 失败标记
*/
Integer FAIL = 1;
/**
* 刷新
*/
String REFRESH_TOKEN = "refresh_token";
/**
* 验证码有效期
*/
int CODE_TIME = 60;
/**
* 验证码长度
*/
String CODE_SIZE = "4";
/**
* 角色前缀
*/
String ROLE = "ROLE_";
/**
* 前缀
*/
String GCZJT_PREFIX = "gczjt_";
/**
* oauth 相关前缀
*/
String OAUTH_PREFIX = "oauth:";
/**
* 项目的license
*/
String GCZJT_LICENSE = "made by gczjt";
/**
* 内部
*/
String FROM_IN = "Y";
/**
* 标志
*/
String FROM = "from";
/**
* OAUTH URL
*/
String OAUTH_TOKEN_URL = "/oauth/token";
/**
* 手机号登录URL
*/
String SMS_TOKEN_URL = "/mobile/token/sms";
/**
* 社交登录URL
*/
String SOCIAL_TOKEN_URL = "/mobile/token/social";
/**
* 自定义登录URL
*/
String MOBILE_TOKEN_URL = "/mobile/token/*";
/**
* 微信获取OPENID
*/
String WX_AUTHORIZATION_CODE_URL = "https://api.weixin.qq.com/sns/oauth2/access_token"
+ "?appid=%s&secret=%s&code=%s&grant_type=authorization_code";
/**
* 微信小程序OPENID
*/
String MINI_APP_AUTHORIZATION_CODE_URL = "https://api.weixin.qq.com/sns/jscode2session"
+ "?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code";
/**
* 码云获取token
*/
String GITEE_AUTHORIZATION_CODE_URL = "https://gitee.com/oauth/token?grant_type="
+ "authorization_code&code=%S&client_id=%s&redirect_uri=" + "%s&client_secret=%s";
/**
* 开源中国获取token
*/
String OSC_AUTHORIZATION_CODE_URL = "https://www.oschina.net/action/openapi/token";
/**
* 码云获取用户信息
*/
String GITEE_USER_INFO_URL = "https://gitee.com/api/v5/user?access_token=%s";
/**
* 开源中国用户信息
*/
String OSC_USER_INFO_URL = "https://www.oschina.net/action/openapi/user?access_token=%s&dataType=json";
/**
* {bcrypt} 加密的特征码
*/
String BCRYPT = "{bcrypt}";
/**
* sys_oauth_client_details 表的字段,不包括client_id、client_secret
*/
String CLIENT_FIELDS = "client_id, CONCAT('{noop}',client_secret) as client_secret, resource_ids, scope, "
+ "authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, "
+ "refresh_token_validity, additional_information, autoapprove";
/**
* JdbcClientDetailsService 查询语句
*/
String BASE_FIND_STATEMENT = "select " + CLIENT_FIELDS + " from sys_oauth_client_details";
/**
* 按条件client_id 查询
*/
String DEFAULT_SELECT_STATEMENT = BASE_FIND_STATEMENT + " where client_id = ? and del_flag = 0 and tenant_id = %s";
/**
* 资源服务器默认bean名称
*/
String RESOURCE_SERVER_CONFIGURER = "resourceServerConfigurerAdapter";
/**
* 客户端模式
*/
String CLIENT_CREDENTIALS = "client_credentials";
/**
* 用户ID字段
*/
String DETAILS_USER_ID = "id";
/**
* 用户名
*/
String DETAILS_USERNAME = "username";
/**
* 信息是否完善
*/
String DETAILS_USERISFULL = "isFull";
/**
* 用户基本信息
*/
String DETAILS_USER = "user_info";
/**
* 用户名phone
*/
String DETAILS_PHONE = "phone";
/**
* 头像
*/
String DETAILS_AVATAR = "avatar";
/**
* 用户部门字段
*/
String DETAILS_DEPT_ID = "deptId";
/**
* 租户ID 字段
*/
String DETAILS_TENANT_ID = "tenantId";
/**
* 协议字段
*/
String DETAILS_LICENSE = "license";
/**
* 激活字段 兼容外围系统接入
*/
String ACTIVE = "active";
/**
* AES 加密
*/
String AES = "aes";
}
微信绑定
import com.hzjtcl.commons.tools.utils.Result;
import com.hzjtcl.service.SysSocialDetailsService;
import io.swagger.annotations.Api;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 系统社交登录账号表
*
* @author gczjt
* @date 2018-08-16 21:30:41
*/
@RestController
@RequestMapping("/social")
@AllArgsConstructor
@Api(value = "social", tags = "三方账号管理模块")
public class SysSocialDetailsController {
private final SysSocialDetailsService sysSocialDetailsService;
/**
* 绑定社交账号
* @param state 类型
* @param code code
* @return
*/
@PostMapping("/bind")
public Result bindSocial(String state, String code) {
Boolean aBoolean = sysSocialDetailsService.bindSocial(state, code);
if (aBoolean){
return new Result().ok(true);
}else{
return new Result().error("绑定失败!");
}
}
}
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.hzjtcl.commons.mybatis.service.BaseService;
import com.hzjtcl.entity.SysSocialDetailsEntity;
/**
* 系统社交登录账号表
*
* @author gczjt
* @date 2018-08-16 21:30:41
*/
public interface SysSocialDetailsService extends BaseService<SysSocialDetailsEntity> {
/**
* 绑定社交账号
* @param state 类型
* @param code code
* @return
*/
Boolean bindSocial(String state, String code);
}
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.hzjtcl.commons.mybatis.service.impl.BaseServiceImpl;
import com.hzjtcl.commons.security.user.SecurityUser;
import com.hzjtcl.dao.SysSocialDetailsDao;
import com.hzjtcl.entity.SysSocialDetailsEntity;
import com.hzjtcl.entity.SysUserEntity;
import com.hzjtcl.enums.CacheConstants;
import com.hzjtcl.enums.LoginTypeEnum;
import com.hzjtcl.handler.LoginHandler;
import com.hzjtcl.service.SysSocialDetailsService;
import com.hzjtcl.service.SysUserService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import java.util.Map;
/**
* @author gczjt
* @date 2018年08月16日
*/
@Slf4j
@AllArgsConstructor
@Service
public class SysSocialDetailsServiceImpl extends BaseServiceImpl<SysSocialDetailsDao, SysSocialDetailsEntity>
implements SysSocialDetailsService {
private final Map<String, LoginHandler> loginHandlerMap;
private final CacheManager cacheManager;
private final SysUserService sysUserService;
/**
* 绑定社交账号
* @param type type
* @param code code
* @return
*/
@Override
public Boolean bindSocial(String type, String code) {
LoginHandler loginHandler = loginHandlerMap.get(type);
String identify = loginHandler.identify(code);
if (StringUtils.isNotBlank(identify)) {
SysUserEntity user = sysUserService.selectById(SecurityUser.getUserId());
// if (LoginTypeEnum.GITEE.getType().equals(type)) {
// sysUser.setGiteeLogin(identify);
// }
// else if (LoginTypeEnum.OSC.getType().equals(type)) {
// sysUser.setOscId(identify);
// }
if (LoginTypeEnum.WECHAT.getType().equals(type)) {
user.setWxOpenid(identify);
}
// else if (LoginTypeEnum.QQ.getType().equals(type)) {
// sysUser.setQqOpenid(identify);
// }
else if (LoginTypeEnum.MINI_APP.getType().equals(type)) {
user.setMiniOpenid(identify);
}
sysUserService.updateById(user);
// 更新緩存
cacheManager.getCache(CacheConstants.USER_DETAILS).evict(user.getUsername());
return Boolean.TRUE;
}else{
return Boolean.FALSE;
}
}
}
微信绑定和登录思路总结:
该系统使用微信登录之前 必须先进行微信绑定, 如果未绑定则提示用户进行微信绑定;
使用微信登录和微信绑定需要公司在微信开放平台进行注册,得到注册后的专属url和appId
前端通过appId和url进行微信接口访问得到 返回值 code
通过该code 和 登录类型 对用户进行 openID 的绑定,
openId需要 appId 和 code参数 再次对微信平台进行访问得到返回值 openId
信息绑定成功之后
登录时前端传入特定的参数, 进行参数的解析, 同样需要微信平台的code值
通过该code值和appId再次获取 openId ,通过openId来获取用户信息进行登录.
核心逻辑就是对微信接口访问得到code值和openId, 将openId与用户进行绑定,
登录时通过该openId获取用户信息.
前端代码
wxlogin() {
// ElMessage.success("测试中,敬请期待");
// 该路径为微信访问成功之后回调路径, 需在微信开放平台进行注册
const redirect_uri = encodeURIComponent("http://oa.hebhzjt.com:7879/#/authredirect");
// appid
const appid = "wx549f52db7fc1da3b";
// 该路由参数参照 微信开放平台api
let url = `https://open.weixin.qq.com/connect/qrconnect?appid=${appid}&redirect_uri=${redirect_uri}&state=WX-LOGIN&response_type=code&scope=snsapi_login#wechat_redirect`;
window.open(url);
},
// 登录接口调用
onLoginBySocial(code) {
baseService
.post(
"/auth/mobile/token/social",
// 登录参数 code 为微信调用回调值 wx@用于后台通过 '@'进行参数解析获取登录类型
{ mobile: `WX@${code}`, grant_type: "mobile" },
{
"content-type": "application/x-www-form-urlencoded",
Authorization: "Basic aHpqdGNsOmh6anRjbA=="
}
)
.then((res) => {
if (res.code === 0) {
setCache(CacheToken, res, true);
ElMessage.success(this.$t("ui.login.loginOk"));
this.$router.push("/");
} else {
ElMessage.error(res.msg);
this.onRefreshCode();
}
})
.catch(() => {
ElMessage.error("未绑定登录账号,请使用密码登录后绑定");
this.loading = false;
});
},