认证授权模块 - 用户认证
文章目录
- 认证授权模块 - 用户认证
- 一、用户认证 基础
- 1.1 连接数据库认证
- 1.1.0 用户表 xc_user
- 1.1.1 分析
- 1.1.2 安全管理配置 WebSecurityConfig
- 1.1.3 自定义UserDetailsService
- 1.1.4 授权服务器配置 AuthorizationServer
- 1.2 扩展用户身份信息
- 1.2.1 修改 UserServiceImpl
- 1.2.2 用户身份信息
- 1.3 工具类获取用户身份
- 二、统一认证入口
- 2.1 认证用户请求参数 AuthParamsDto
- 2.2 修改 UserServiceImpl
- 2.3 重写DaoAuthenticationProvider
- 2.4 修改 WebSecurityConfig 安全配置管理
- 2.5 统一认证接口 AuthService
- 2.5.1 XcUserExt 用户扩展信息
- 2.5.2 修改 UserServiceImpl
- 2.5.3 统一认证接口
- 三、部署验证码服务
- 3.1 连接 Redis
- 3.2 验证码流程
- 3.3 验证码服务接口
- 四、 账号密码登录
- 4.1 PasswordAuthServiceImpl
- 4.2 修改 UserServiceImpl
- 4.3 Feign 接口
- 4.3.1 CheckCodeClient
- 4.3.2 CheckCodeClientFactory
- 4.3.3 开启Feign
- 4.4 测试
- 五、微信扫码验证
- 5.1 介绍
- 5.1.1 请求获取授权码
- 5.1.2 通过code获取access_token
- 5.1.3 通过access_token调用接口
- 5.2 接入
- 5.2.1 接入分析
- 5.1.2 WxLoginController 接口定义
- 5.2.3 WxAuthServiceImpl
- 5.2.4 xc_user_role 学生角色关系表
- 5.2.5 xc_role 角色表
一、用户认证 基础
依然是xuecheng-auth模块
实现如下图所示的流程:
我们要实现的其实就是下图中圈出来的部分,最重要的就是统一认证入口
认证所需要的用户信息存储在用户中心数据库,现在需要将认证服务连接数据库查询用户信息
1.1 连接数据库认证
1.1.0 用户表 xc_user
写在auth工程中
wx_unionid是在微信开放平台上唯一的id
张三扫描微信二维码登录后会有一个unionid
李四扫描微信二维码登录后会有一个unionid
其实相当于用户id了
实体类
@Data
@TableName("xc_user")
public 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;
}
1.1.1 分析
一般认证就是用户输入账号和密码,判断用户是否存在和判断密码是否正确,有时候可能还会需要验证码
登录界面
http://www.51xuecheng.cn/sign.html
认证用户的流程如下图所示
- 一个请求过来之后首先到我们SpringSecurity框架认证管理器AuthenticationManager
- DaoAuthenticationProvider就是用来查询数据库的,会通过loadUserByUsername()获取用户信息,并封装成一个UserDetails类型的对象
loadUserByUsername方法是UserDetailsService接口的方法
简单来说:用户提交账号和密码由DaoAuthenticationProvider调用UserDetailsService的loadUserByUsername()方法获取UserDetails用户信息
- UserDetailsService接口内容
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
我们只要实现UserDetailsService 接口查询数据库得到用户信息返回UserDetails 类型的用户信息即可,框架自己调用loadUserByUsername()方法拿到用户信息之后执行逻辑如下图所示
- 查询DaoAuthenticationProvider的源代码如下:
- UserDetails用户信息接口
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
1.1.2 安全管理配置 WebSecurityConfig
下面的代码是在内存中配置的用户信息,将其删掉,因为我们要从数据库中查询
//配置用户信息服务
@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;
}
配置密码加密方式
@Bean
public PasswordEncoder passwordEncoder() {
//密码为明文方式
//return NoOpPasswordEncoder.getInstance();
//密码为加密之后
return new BCryptPasswordEncoder();
}
其他配置
//配置安全拦截机制
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/r/**").authenticated()//访问/r开始的请求需要认证通过
.anyRequest().permitAll()//其它请求全部放行
.and()
.formLogin().successForwardUrl("/login-success");//登录成功跳转到/login-success
}
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
1.1.3 自定义UserDetailsService
写在auth工程中
下面这篇文章和此处获取UserDetails对象的方式不同,可能是因为包不同的原因
SpringSecurity - 基于数据库认证与授权
在目前1.1.3所使用的security包中无法使用下面的代码
SecurityUser securityUser = new SecurityUser(sysUser);
@Component
public class UserServiceImpl implements UserDetailsService {
//注入,将来查询对象
@Autowired
XcUserMapper xcUserMapper;
/**
* @param s 其实就是输入的username(账号)
*/
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// 1.根据username账号查询数据库
// 因为账号是不可能重复的,直接selectOne即可
LambdaQueryWrapper<XcUser> lqw = new LambdaQueryWrapper<>();
lqw.eq(XcUser::getUsername, s);
XcUser xcUser = xcUserMapper.selectOne(lqw);
// 2.查询不到用户,返回null即可,SpringSecurity框架会自动抛出异常“用户不存在”
if (xcUser == null) {
return null;
}
// 3.如果查到了用户并查询到正确的密码,将用户信息封装成UserDetails类型数据返回,SpringSecurity框架会比对密码是否正确,我们不用比对密码
String password = xcUser.getPassword();
UserDetails userDetails = User.withUsername(s)
//用户密码
.password(password)
//用户权限,暂时先不写
.authorities("").build();
return userDetails;
}
}
1.1.4 授权服务器配置 AuthorizationServer
将客户端秘钥改成密文,不再是明文“XcWebApp”
//客户端详情服务
@Override
public void configure(ClientDetailsServiceConfigurer clients)
throws Exception {
clients.inMemory()// 使用in-memory存储
.withClient("XcWebApp")// client_id
// .secret("XcWebApp")//客户端密钥
.secret(new BCryptPasswordEncoder().encode("XcWebApp"))//客户端密钥
.resourceIds("xuecheng-plus")//资源列表
.authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")// 该client允许的授权类型authorization_code,password,refresh_token,implicit,client_credentials
.scopes("all")// 允许的授权范围
.autoApprove(false)//false跳转到授权页面
//客户端接收授权码的重定向地址
.redirectUris("http://www.51xuecheng.cn")
;
}
1.2 扩展用户身份信息
用户表中存储了用户的账号、手机号、email,昵称、qq等信息,UserDetails接口只返回了username、密码等信息
如果只返回一个“user_name”字段是不够用的
SpringSecurity中UserDetails接口如下所示
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
如何扩展Spring Security的用户身份信息呢?
在认证阶段DaoAuthenticationProvider会调用UserDetailService查询用户的信息,这里是可以获取到齐全的用户信息的。
由于JWT令牌中用户身份信息来源于UserDetails,UserDetails中仅定义了username为用户的身份信息,
这里有两个思路:
第一是可以扩展UserDetails,使之包括更多的自定义属性
第二也可以扩展username的内容 ,比如存入json数据内容作为username的内容。
相比较而言,方案二比较简单还不用破坏UserDetails的结构,我们采用方案二
1.2.1 修改 UserServiceImpl
写在auth工程中
/**
* @param s 其实就是输入的username(账号)
*/
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// 1.根据username账号查询数据库
// 因为账号是不可能重复的,直接selectOne即可
LambdaQueryWrapper<XcUser> lqw = new LambdaQueryWrapper<>();
lqw.eq(XcUser::getUsername, s);
XcUser xcUser = xcUserMapper.selectOne(lqw);
// 2.查询不到用户,返回null即可,SpringSecurity框架会自动抛出异常“用户不存在”
if (xcUser == null) {
return null;
}
// 3.如果查到了用户并查询到正确的密码,将用户信息封装成UserDetails类型数据返回,SpringSecurity框架会比对密码是否正确,我们不用比对密码
String password = xcUser.getPassword();
//用户权限,如果不加报Cannot pass a null GrantedAuthority collection
String[] authorities = {"p1"};
xcUser.setPassword(""); // 为了安全考虑,专JSON前吧密码做空
String userString = JSON.toJSONString(xcUser);
UserDetails userDetails = User.withUsername(userString)
//用户密码
.password(password)
//用户权限,暂时先不写
.authorities(authorities).build();
return userDetails;
}
1.2.2 用户身份信息
user_name存储了用户信息的json格式,在资源服务中就可以取出该json格式的内容转为用户对象去使用
1.3 工具类获取用户身份
下边编写一个工具类在各个微服务中去使用,获取当前登录用户的对象
比如我们在content模块要获取用户身份,那我们就把工具类放在content模块下
/**
* @description 获取当前用户身份工具类
*/
@Slf4j
public class SecurityUtil {
public static XcUser getUser() {
try {
// getContext() 获取上下文对象,getAuthentication()拿到认证信息,getPrincipal()拿到身份信息
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;
}
// 内部类 因为XcUser是在auth服务中,并不在content服务中,所以在这里我们使用了一个内部类
// 其实是完全一样的
@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;
}
}
使用
@ApiOperation("修改课程接口")
@PutMapping("/course")
public CourseBaseInfoDto getCourseBaseById(@RequestBody EditCourseDto editCourseDto) {
// getContext() 获取上下文对象,getAuthentication()拿到认证信息,getPrincipal()拿到身份信息
//SecurityContextHolder.getContext().getAuthentication().getPrincipal();
SecurityUtil.XcUser user = SecurityUtil.getUser();
System.out.println(user.getUsername());
//机构id先写死,后面授权认证的时候后会改过来
Long companyId = 1232141425L;
return courseBaseInfoService.updateCourseBase(companyId, editCourseDto);
}
二、统一认证入口
其实也就是登录界面
2.1 认证用户请求参数 AuthParamsDto
写在auth工程中
用户名密码登录、短信登录等方式都是用下面实体类作为请求参数
/**
* @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.2 修改 UserServiceImpl
其实就是将参数“s”,转成了一个
@Component
public class UserServiceImpl implements UserDetailsService {
//注入,将来查询对象
@Autowired
XcUserMapper xcUserMapper;
/**
* @param s 传入的请求认证的参数是AuthParamDtoJSON串,此时不再是输入的username(账号)
*/
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// 将传入的JSON转成AuthParamsDto对象
AuthParamsDto authParamsDto = null;
try {
authParamsDto = JSON.parseObject(s, AuthParamsDto.class);
} catch (Exception e) {
throw new RuntimeException("请求认证的参数不符合要求");
}
String username = authParamsDto.getUsername();
// 1.根据username账号查询数据库
// 因为账号是不可能重复的,直接selectOne即可
LambdaQueryWrapper<XcUser> lqw = new LambdaQueryWrapper<>();
lqw.eq(XcUser::getUsername, username);
XcUser xcUser = xcUserMapper.selectOne(lqw);
// 2.查询不到用户,返回null即可,SpringSecurity框架会自动抛出异常“用户不存在”
if (xcUser == null) {
return null;
}
// 3.如果查到了用户并查询到正确的密码,将用户信息封装成UserDetails类型数据返回,SpringSecurity框架会比对密码是否正确,我们不用比对密码
String password = xcUser.getPassword();
//用户权限,如果不加报Cannot pass a null GrantedAuthority collection
String[] authorities = {"p1"};
xcUser.setPassword(""); // 为了安全考虑,专JSON前吧密码做空
String userString = JSON.toJSONString(xcUser);
UserDetails userDetails = User.withUsername(userString)
//用户密码
.password(password)
//用户权限,暂时先不写
.authorities(authorities).build();
return userDetails;
}
}
修改前的请求方式
### 密码模式
POST {{auth_host}}/auth/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username=t1&password=111111
修改后的请求方式
### 密码模式,请求AuthParamsDto参数
POST {{auth_host}}/auth/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username={"username":"t1","password":"111111","authType":"password"}
2.3 重写DaoAuthenticationProvider
我们原先在UserServiceImpl中返回了一个UserDetails对象,框架会自动帮我们校验密码是否正确
但是我们除了用户名及密码登录外,还有短信验证码登录、微信登录等,这些登录方式框架是帮我们完成不了的,所以我们需要重写一下
简单来说,不是所有的登录都需要验证密码,短信登录的时候就不需要验证密码
怎么实现所说的业务要求呢?
按照下图所示流程图,我们重写DaoAuthenticationProvider即可
/**
* 继承DaoAuthenticationProvider的目的就是重写其校验密码的方法
* 因为我们统一了认证的入口,有一些认证方式不需要校验密码
*/
@Slf4j
@Component
public class DaoAuthenticationProviderCustom extends DaoAuthenticationProvider {
/**
* 重写校验密码的方式
* @param userDetailsService
*/
@Autowired
public void setUserDetailsService(UserDetailsService userDetailsService) {
super.setUserDetailsService(userDetailsService);
}
//屏蔽密码对比,设置为空就行了
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
}
}
2.4 修改 WebSecurityConfig 安全配置管理
@Autowired
DaoAuthenticationProviderCustom daoAuthenticationProviderCustom;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(daoAuthenticationProviderCustom);
}
2.5 统一认证接口 AuthService
在2.5 完成后,我们要实现下面这个流程结构
2.5.1 XcUserExt 用户扩展信息
其实就是用户的权限信息
/**
* @description 用户扩展信息
*/
@Data
public class XcUserExt extends XcUser {
//用户权限
List<String> permissions = new ArrayList<>();
}
2.5.2 修改 UserServiceImpl
// 注入Spring上下文对象
@Autowired
ApplicationContext applicationContext;
/**
* @param s 传入的请求认证的参数是AuthParamDtoJSON串,此时不再是输入的username(账号)
*/
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// 将传入的JSON转成AuthParamsDto对象
AuthParamsDto authParamsDto = null;
try {
authParamsDto = JSON.parseObject(s, AuthParamsDto.class);
} catch (Exception e) {
throw new RuntimeException("请求认证的参数不符合要求");
}
// 判断登录的类型(认证类型,有password、wx)
String authType = authParamsDto.getAuthType();
String beanName = authType +"_authservice";
// 取出指定的bean
AuthService authService = applicationContext.getBean(beanName, AuthService.class);
// 调用统一认证方法完成认证
authService.execute(authParamsDto);
// 省略下面的代码
............................
}
2.5.3 统一认证接口
/**
* 统一认证接口:判断账号是否存在、密码是否正确
*/
public interface AuthService {
/**
* @description 认证方法
* @param authParamsDto 认证参数
* @return com.xuecheng.ucenter.model.po.XcUser 用户信息
*/
XcUserExt execute(AuthParamsDto authParamsDto);
}
三、部署验证码服务
此模块也是一个微服务,checkcode模块
验证码可以防止恶性攻击
比如:XSS跨站脚本攻击、CSRF跨站请求伪造攻击,一些比较复杂的图形验证码可以有效的防止恶性攻击
为了保护系统的安全在一些比较重要的操作都需要验证码
验证码的类型也有很多:图片、语音、手机短信验证码等
本项目创建单独的验证码服务为各业务提供验证码的生成、校验等服务
3.1 连接 Redis
- 在nacos中配置redis-dev.yaml配置文件
spring:
redis:
host: 192.168.101.65
port: 6379
password: redis
database: 0
lettuce:
pool:
max-active: 20
max-idle: 10
min-idle: 0
timeout: 10000
#redisson:
#配置文件目录
#config: classpath:singleServerConfig.yaml
- checkcode服务模块引入redis-dev.yaml配置文件
spring:
application:
name: checkcode
cloud:
nacos:
server-addr: 192.168.101.65:8848
discovery:
namespace: dev
group: xuecheng-plus-project
config:
namespace: dev
group: xuecheng-plus-project
file-extension: yaml
refresh-enabled: true
shared-configs:
- data-id: swagger-${spring.profiles.active}.yaml
group: xuecheng-plus-common
refresh: true
- data-id: logging-${spring.profiles.active}.yaml
group: xuecheng-plus-common
refresh: true
- data-id: redis-${spring.profiles.active}.yaml
group: xuecheng-plus-common
refresh: true
profiles:
active: dev
3.2 验证码流程
- 首先申请验证码
### 申请验证码
POST {{checkcode_host}}/checkcode/pic
返回值如下图所示
分别返回的是验证码图片的key值和验证码图片的base64值
{
"key": "checkcode:ee3f4d9308534cbf9fe4672b34787d30",
"aliasing": "data:image/png;base64,iVBORw0KGgoAAAA......"
}
- 校验认证码
### 校验验证码
POST {{checkcode_host}}/checkcode/verify?key=checkcode:ee3f4d9308534cbf9fe4672b34787d30&code=fbl4
下面是验证码使用流程图示
3.3 验证码服务接口
/**
* @author Mr.M
* @version 1.0
* @description 验证码服务接口
* @date 2022/9/29 18:39
*/
@Api(value = "验证码服务接口")
@RestController
public class CheckCodeController {
@Resource(name = "PicCheckCodeService")
private CheckCodeService picCheckCodeService;
@ApiOperation(value = "生成验证信息", notes = "生成验证信息")
@PostMapping(value = "/pic")
public CheckCodeResultDto generatePicCheckCode(CheckCodeParamsDto checkCodeParamsDto) {
return picCheckCodeService.generate(checkCodeParamsDto);
}
@ApiOperation(value = "校验", notes = "校验")
@ApiImplicitParams({
@ApiImplicitParam(name = "name", value = "业务名称", required = true, dataType = "String", paramType = "query"),
@ApiImplicitParam(name = "key", value = "验证key", required = true, dataType = "String", paramType = "query"),
@ApiImplicitParam(name = "code", value = "验证码", required = true, dataType = "String", paramType = "query")
})
@PostMapping(value = "/verify")
public Boolean verify(String key, String code) {
Boolean isSuccess = picCheckCodeService.verify(key, code);
return isSuccess;
}
}
四、 账号密码登录
4.1 PasswordAuthServiceImpl
统一认证接口AuthService的实现类PasswordAuthServiceImpl
在此实现类中实现账号和密码登录方式
AuthService接口会有多个实现类,下面是实现类PasswordAuthServiceImpl
/**
* 处理用户名密码登录验证
*/
@Service("password_authservice")
public class PasswordAuthServiceImpl implements AuthService {
@Autowired
XcUserMapper xcUserMapper;
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
CheckCodeClient checkCodeClient;
@Override
public XcUserExt execute(AuthParamsDto authParamsDto) {
// 1.账号
String username = authParamsDto.getUsername();
// 2.校验验证码(校验认证码的服务在checkcode模块,利用feign向checkcode发送请求)
String checkcode = authParamsDto.getCheckcode();
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("验证码输入错误");
}
// 3.校验账号是否存在
// 3.1 根据username账号查询数据库
// 因为账号是不可能重复的,直接selectOne即可
LambdaQueryWrapper<XcUser> lqw = new LambdaQueryWrapper<>();
lqw.eq(XcUser::getUsername, username);
XcUser xcUser = xcUserMapper.selectOne(lqw);
// 3.2 查询不到用户,返回null即可
if (xcUser == null) {
throw new RuntimeException("账号不存在");
}
// 4.验证密码是否正确
// 用户真正的密码
String password = xcUser.getPassword();
// 用户输入的密码
String passwordForm = authParamsDto.getPassword();
// 校验密码
boolean matches = passwordEncoder.matches(passwordForm, password);
if (!matches) {
throw new RuntimeException("用户账号或密码错误");
}
// 5.这个地方到授权后会再改,先这么写着
XcUserExt xcUserExt = new XcUserExt();
BeanUtils.copyProperties(xcUser, xcUserExt);
return xcUserExt;
}
}
4.2 修改 UserServiceImpl
@Component
public class UserServiceImpl implements UserDetailsService {
//注入,将来查询对象
@Autowired
XcUserMapper xcUserMapper;
@Autowired
ApplicationContext applicationContext;
/**
* @param s 传入的请求认证的参数是AuthParamDtoJSON串,此时不再是输入的username(账号)
*/
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// 1.将传入的JSON转成AuthParamsDto对象
AuthParamsDto authParamsDto = null;
try {
authParamsDto = JSON.parseObject(s, AuthParamsDto.class);
} catch (Exception e) {
throw new RuntimeException("请求认证的参数不符合要求");
}
// 2.判断登录的类型(认证类型,有password、wx)
String authType = authParamsDto.getAuthType();
String beanName = authType + "_authservice";
// 取出指定的bean
AuthService authService = applicationContext.getBean(beanName, AuthService.class);
// 3.调用统一认证方法完成认证
XcUserExt xcUserExt = authService.execute(authParamsDto);
// 4.封装XcUserExt为UserDetails类型
UserDetails userDetails = this.getUserPrincipal(xcUserExt);
return userDetails;
}
/**
* 将XcUserExt数据封装成UserDetails数据
*
* @param xcUserExt 用户id,主键
* @return com.xuecheng.ucenter.model.po.XcUser 用户信息
* @description 查询用户信息
*/
public UserDetails getUserPrincipal(XcUserExt xcUserExt) {
String password = xcUserExt.getPassword();
//用户权限,如果不加报Cannot pass a null GrantedAuthority collection
String[] authorities = {"p1"};
xcUserExt.setPassword(""); // 为了安全考虑,专JSON前吧密码做空
String userString = JSON.toJSONString(xcUserExt);
UserDetails userDetails = User.withUsername(userString)
//用户密码
.password(password)
//用户权限,暂时先不写
.authorities(authorities).build();
return userDetails;
}
}
4.3 Feign 接口
远程调用checkcode服务校验验证码
4.3.1 CheckCodeClient
// value 服务名
// fallbackFactory 降级方法执行逻辑
@FeignClient(value = "checkcode", fallbackFactory = CheckCodeClientFactory.class)
@RequestMapping("/checkcode")
public interface CheckCodeClient {
@PostMapping(value = "/verify")
public Boolean verify(@RequestParam("key") String key, @RequestParam("code") String code);
}
4.3.2 CheckCodeClientFactory
/**
* 编写降级方法执行逻辑
*/
@Slf4j
@Component
public class CheckCodeClientFactory implements FallbackFactory<CheckCodeClient> {
@Override
public CheckCodeClient create(Throwable throwable) {
return new CheckCodeClient() {
@Override
public Boolean verify(String key, String code) {
log.debug("调用验证码服务熔断异常:{}", throwable.getMessage());
return null;
}
};
}
}
4.3.3 开启Feign
启动类添加如下注解
@EnableFeignClients(basePackages={"com.xuecheng.ucenter.service.feignclient"})
4.4 测试
点击登录
登录成功后进入到下图所示界面
五、微信扫码验证
微信扫码登录基于OAuth2协议的授权码模式,
接口文档:
https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html
点击微信图标
进行扫码注册
5.1 介绍
接口文档:微信开放平台
根据文档的指示,很显然我们使用的这种方式,仔细阅读一下文档就好了
5.1.1 请求获取授权码
第三方使用网站应用授权登录前请注意已获取相应网页授权作用域(scope=snsapi_login),则可以通过在 PC 端打开以下链接: https://open.weixin.qq.com/connect/qrconnect?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
若提示“该链接无法访问”,请检查参数是否填写错误
如redirect_uri的域名与审核时填写的授权域名不一致或 scope 不为snsapi_login。
返回说明
用户允许授权后,将会重定向到redirect_uri的网址上,并且带上 code 和state参数
redirect_uri?code=CODE&state=STATE
若用户禁止授权,则不会发生重定向。
登录一号店网站应用 https://test.yhd.com/wechat/login.do 打开后,一号店会生成 state 参数,跳转到 https://open.weixin.qq.com/connect/qrconnect?appid=wxbdc5610cc59c1631&redirect_uri=https%3A%2F%2Fpassport.yhd.com%2Fwechat%2Fcallback.do&response_type=code&scope=snsapi_login&state=3d6be0a4035d839573b04816624a415e#wechat_redirect
微信用户使用微信扫描二维码并且确认登录后,PC端会跳转到 https://test.yhd.com/wechat/callback.do?code=CODE&state=3d6be0a40sssssxxxxx6624a415e
为了满足网站更定制化的需求,我们还提供了第二种获取 code 的方式,支持网站将微信登录二维码内嵌到自己页面中,用户使用微信扫码授权后通过 JS 将code返回给网站。
JS微信登录主要用途:网站希望用户在网站内就能完成登录,无需跳转到微信域下登录后再返回,提升微信登录的流畅性与成功率。 网站内嵌二维码微信登录 JS 实现办法:
步骤1:在页面中先引入如下 JS 文件(支持https):
http://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js
步骤2:在需要使用微信登录的地方实例以下 JS 对象:
var obj = new WxLogin({
self_redirect:true,
id:"login_container",
appid: "",
scope: "",
redirect_uri: "",
state: "",
style: "",
href: ""
});
5.1.2 通过code获取access_token
通过 code 获取access_token
https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
返回说明
{
"access_token":"ACCESS_TOKEN",
"expires_in":7200,
"refresh_token":"REFRESH_TOKEN",
"openid":"OPENID",
"scope":"SCOPE",
"unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
}
参数说明
{"errcode":40029,"errmsg":"invalid code"}
5.1.3 通过access_token调用接口
获取access_token后,进行接口调用,有以下前提
- access_token有效且未超时
- 微信用户已授权给第三方应用帐号相应接口作用域(scope)
对于接口作用域(scope),能调用的接口有以下
其中snsapi_base属于基础接口,若应用已拥有其它 scope 权限,则默认拥有snsapi_base的权限。使用snsapi_base可以让移动端网页授权绕过跳转授权登录页请求用户授权的动作,直接跳转第三方网页带上授权临时票据(code),但会使得用户已授权作用域(scope)仅为snsapi_base,从而导致无法获取到需要用户授权才允许获得的数据和基础功能。 接口调用方法可查阅《微信授权关系接口调用指南》
获取用户信息接口文档:https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Authorized_Interface_Calling_UnionID.html
接口地址
http请求方式: GET
https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID
响应
{
"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"
}
说明
参数 说明
openid 普通用户的标识,对当前开发者帐号唯一
nickname 普通用户昵称
sex 普通用户性别,1为男性,2为女性
province 普通用户个人资料填写的省份
city 普通用户个人资料填写的城市
country 国家,如中国为CN
headimgurl 用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空
privilege 用户特权信息,json数组,如微信沃卡用户为(chinaunicom)
unionid 用户统一标识。针对一个微信开放平台帐号下的应用,同一用户的 unionid 是唯一的。
5.2 接入
5.2.1 接入分析
接入微信扫码登录的流程如下
本项目认证服务需要做哪些事?
1、需要定义接口接收微信下发的授权码。
2、收到授权码调用微信接口申请令牌。
3、申请到令牌调用微信获取用户信息
4、获取用户信息成功将其写入本项目用户中心数据库。
5、最后重定向到浏览器自动登录
5.1.2 WxLoginController 接口定义
说明:
用户扫码与授权都是在前端和微信进行交互的
前端在和微信对接扫码时,会设置一个重定向地址,这个地址就是后端某个接口的请求地址,微信就会向这个地方发送授权码,后端接口获取到授权码后就可以完成很多操作
当后端接口收到来自微信的请求后,便可以完成一系列“重定向、授权码、申请令牌”等等一系列操作,最终实现用户的登录
/**
* 微信回调此接口,向服务传送一个授权码
* <p>
* 在此接口内我们要:远程调用微信令牌,拿到令牌查询用户信息,将用户信息写入本项目数据库
*
* @param code 授权码
* @param state 用于保持请求和回调的状态,授权请求后原样带回给第三方。该参数可用于防止csrf 攻击(跨站请求伪造攻击),建议第三方带上该参数,可设置为简单的随机数加 session 进行校验
* @return
* @throws IOException
*/
@RequestMapping("/wxLogin")
public String wxLogin(String code, String state) throws IOException {
log.debug("微信扫码回调,code:{},state:{}", code, state);
//请求微信申请令牌,拿到令牌查询用户信息,将用户信息写入本项目数据库
XcUser xcUser = wxAuthService.wxAuth(code);
//暂时硬编写,目的是调试环境
xcUser.setUsername("t1");
if (xcUser == null) {
return "redirect:http://www.51xuecheng.cn/error.html";
}
String username = xcUser.getUsername();
// 重定向到登录界面自动登录 (注意!authType的值是wx)
return "redirect:http://www.51xuecheng.cn/sign.html?username=" + username + "&authType=wx";
}
5.2.3 WxAuthServiceImpl
在编写之前先注入这个Bean
@Bean
RestTemplate restTemplate(){
RestTemplate restTemplate = new RestTemplate(new OkHttp3ClientHttpRequestFactory());
return restTemplate;
}
定义一个WxAuthService接口
/**
* @description 微信扫码认证
*/
@Slf4j
@Service("wx_authservice")
public class WxAuthServiceImpl implements AuthService, WxAuthService {
@Autowired
XcUserMapper xcUserMapper;
@Autowired
RestTemplate restTemplate;
@Autowired
XcUserRoleMapper xcUserRoleMapper;
@Autowired
WxAuthServiceImpl currentProxy;
// 微信appid
String appid = "wx17655f8047b85150";
// 微信App秘钥
String secret = "68918d65287802a19b1905cbda7eaa93";
@Override
public XcUserExt execute(AuthParamsDto authParamsDto) {
//账号
String username = authParamsDto.getUsername();
XcUser user = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, username));
if (user == null) {
//返回空表示用户不存在
throw new RuntimeException("账号不存在");
}
XcUserExt xcUserExt = new XcUserExt();
BeanUtils.copyProperties(user, xcUserExt);
return xcUserExt;
}
/**
* 微信扫码认证:
* 1.申请令牌
* 2.携带令牌查询用户信息
* 3.保存用户信息到数据库
*
* @param code 微信下发的授权码
* @return
*/
@Override
public XcUser wxAuth(String code) {
// 1.申请令牌
Map<String, String> accessTokenMap = getAccess_token(code);
//令牌
String accessToken = accessTokenMap.get("access_token");
//openid
String openid = accessTokenMap.get("openid");
// 2.携带令牌查询用户信息
Map<String, String> userInfo = this.getUserInfo(accessToken, openid);
// 3.保存用户信息到数据库
// 避免事物失效(一个非事物方法调用事物方法)
XcUser xcUser = currentProxy.addWxUser(userInfo);
return xcUser;
}
/**
* 申请令牌
* 通过code获取access_token
* https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
*
* @param code 微信下发的授权码
* @return 根据文档发现,返回值是一个JSON信息
* {
* "access_token":"ACCESS_TOKEN",
* "expires_in":7200,
* "refresh_token":"REFRESH_TOKEN",
* "openid":"OPENID",
* "scope":"SCOPE",
* "unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
* }
*/
private Map<String, String> getAccess_token(String code) {
String url_template = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code";
String url = String.format(url_template, appid, secret, code);
// 参数三:请求参数设置为null,因为我们拼接在url后面了 参数四:返回值类型,执行String类型就行
ResponseEntity<String> exchange = restTemplate.exchange(url, HttpMethod.POST, null, String.class);
//获取响应结果
String result = exchange.getBody();
Map<String, String> map = JSON.parseObject(result, Map.class);
return map;
}
/**
* 携带令牌查询用户信息
* http请求方式: GET
* https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID
*
* @param access_token 令牌
* @param openid 返回值实例:
* {
* "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"
* }
*/
private Map<String, String> getUserInfo(String access_token, String openid) {
String wxUrl_template = "https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s";
//请求微信地址
String wxUrl = String.format(wxUrl_template, access_token, openid);
log.info("调用微信接口申请access_token, url:{}", wxUrl);
ResponseEntity<String> exchange = restTemplate.exchange(wxUrl, HttpMethod.GET, null, String.class);
//防止乱码进行转码
// 因为返回值格式是8859-1,我们转换成UTF-8,解决乱码问题
String result = new String(exchange.getBody().getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
Map<String, String> map = JSON.parseObject(result, Map.class);
return map;
}
@Transactional
public XcUser addWxUser(Map userInfo_map) {
//wx_unionid是在微信开放平台上唯一的id
//张三扫描微信二维码登录后会有一个unionid,李四扫描微信二维码登录后会有一个unionid
String unionid = userInfo_map.get("unionid").toString();
//根据unionid查询数据库
XcUser xcUser = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getWxUnionid, unionid));
if (xcUser != null) {
//说明查询到对应的用户了
return xcUser;
}
// 没有查询到用户,新增用户
String userId = UUID.randomUUID().toString();
xcUser = new XcUser();
xcUser.setId(userId);
xcUser.setWxUnionid(unionid);
//记录从微信得到的昵称
xcUser.setNickname(userInfo_map.get("nickname").toString());
xcUser.setUserpic(userInfo_map.get("headimgurl").toString());
xcUser.setName(userInfo_map.get("nickname").toString());
xcUser.setUsername(unionid);
xcUser.setPassword(unionid);
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");//学生角色
// 操作的xc_user_role学生角色关系表
xcUserRoleMapper.insert(xcUserRole);
return xcUser;
}
}