JWT:
1、简述:
使用JWT可以实现无状态认证。
传统的基于session的方式是有状态认证,用户登录成功将用户的身份信息存储在服务端,这样加大了服务端的存储压力,并且这种方式不适合在分布式系统中应用。
最主要的原因:存在session共享的问题。
例如我现在有A,B两个服务,A服务的端口是8081,B服务的端口是8082。此时请求A服务,会返回了一个sessionId并且存入了本地Cookie中。第二次访问B服务,此时因为本地Cookie中存在了一个sessionId,此时浏览器会带着A的sessionId去访问B服务,但是B服务通过这个sessionId没有找到相对应的数据,因此它就会创建一个新的sessionId并且响应返回给客户端。
造成的结果就是,A和B服务拿到的永远都是对方的sessionId。
而基于令牌技术在分布式系统中实现认证则服务端不用存储session,可以将用户身份信息存储在令牌中,用户认证通过后认证服务颁发令牌给用户,用户将令牌存储在客户端,去访问应用服务时携带令牌去访问,服务端从jwt解析出用户信息。这个过程就是无状态认证。
2、组成:
JWT令牌由三部分组成,每部分中间使用点(.)分隔
第一部分是头部,包括令牌的类型(即JWT)及使用的哈希算法(如HMAC SHA256或RSA)
第二部分是负载,内容也是一个json对象,它是存放有效信息的地方,它可以存放jwt提供的信息字段,比如:iss(签发者),exp(过期时间戳), sub(面向的用户)等,也可自定义字段。最后将第二部分负载使用Base64Url编码,得到一个字符串就是JWT令牌的第二部分。
第三部分是签名,此部分用于防止jwt内容被篡改。这个部分使用base64url将前两部分进行编码,编码后使用点(.)连接组成字符串,最后使用header中声明的签名算法进行签名。
3、验证流程:
保证签名正确:
3.1、内容、秘钥与签名前保持一致,服务间使用相同的秘钥(对称加密)。
3.2、两个服务间使用不同的秘钥,分为公钥和私钥,但是公钥,私钥需成对(非对称加密)。
4、项目实现
4.1、xuecheng-plus-auth服务生成/校验JWT令牌:
@Configuration
public class TokenConfig {
private String SIGNING_KEY = "mq123";
@Autowired
TokenStore tokenStore;
// @Bean
// public TokenStore tokenStore() {
// //使用内存存储令牌(普通令牌)
// return new InMemoryTokenStore();
// }
@Autowired
private JwtAccessTokenConverter accessTokenConverter;
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY);
return converter;
}
//令牌管理服务
@Bean(name="authorizationServerTokenServicesCustom")
public AuthorizationServerTokenServices tokenService() {
DefaultTokenServices service=new DefaultTokenServices();
service.setSupportRefreshToken(true);//支持刷新令牌
service.setTokenStore(tokenStore);//令牌存储策略
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
service.setTokenEnhancer(tokenEnhancerChain);
service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
return service;
}
}
使用httpclient通过密码模式申请令牌测试:
### 密码模式
POST http://localhost:63070/auth/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username={"username":"admin","authType":"password","password":"123456"}
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsieHVlY2hlbmctcGx1cyJdLCJ1c2VyX25hbWUiOiJ7XCJjcmVhdGVUaW1lXCI6XCIyMDIyLTA5LTI4VDA4OjMyOjAzXCIsXCJpZFwiOlwiNDhcIixcIm5hbWVcIjpcIuezu-e7n-euoeeQhuWRmFwiLFwicGVybWlzc2lvbnNcIjpbXSxcInNleFwiOlwiMVwiLFwic3RhdHVzXCI6XCIxXCIsXCJ1c2VybmFtZVwiOlwiYWRtaW5cIixcInV0eXBlXCI6XCIxMDEwMDNcIn0iLCJzY29wZSI6WyJhbGwiXSwiZXhwIjoxNzA4MDAzMTk0LCJhdXRob3JpdGllcyI6WyJ0ZXN0Il0sImp0aSI6IjI1NDBlMjQ4LWIxZDktNGMyMC1hZjc4LWNlOTk3YWU0YjlmOSIsImNsaWVudF9pZCI6IlhjV2ViQXBwIn0.16p1HJX5VxkQ45Ivany-W5a0854Vl1efTJ8DuavB6aY",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsieHVlY2hlbmctcGx1cyJdLCJ1c2VyX25hbWUiOiJ7XCJjcmVhdGVUaW1lXCI6XCIyMDIyLTA5LTI4VDA4OjMyOjAzXCIsXCJpZFwiOlwiNDhcIixcIm5hbWVcIjpcIuezu-e7n-euoeeQhuWRmFwiLFwicGVybWlzc2lvbnNcIjpbXSxcInNleFwiOlwiMVwiLFwic3RhdHVzXCI6XCIxXCIsXCJ1c2VybmFtZVwiOlwiYWRtaW5cIixcInV0eXBlXCI6XCIxMDEwMDNcIn0iLCJzY29wZSI6WyJhbGwiXSwiYXRpIjoiMjU0MGUyNDgtYjFkOS00YzIwLWFmNzgtY2U5OTdhZTRiOWY5IiwiZXhwIjoxNzA4MjU1MTk0LCJhdXRob3JpdGllcyI6WyJ0ZXN0Il0sImp0aSI6ImVlZDhkMGI1LWRiN2EtNDYxMC05NDNmLTk1MDRlZWZkZWEzOCIsImNsaWVudF9pZCI6IlhjV2ViQXBwIn0.3EE-ppOj7h2Rxq4RJTGNYCvpHMJwCGNnGOkdAVnOSCA",
"expires_in": 7199,
"scope": "all",
"jti": "2540e248-b1d9-4c20-af78-ce997ae4b9f9"
}
access_token,生成的jwt令牌,用于访问资源使用。
token_type,bearer是在RFC6750中定义的一种token类型,在携带jwt访问资源时需要在head中加入bearer jwt令牌内容
refresh_token,当jwt令牌快过期时使用刷新令牌可以再次生成jwt令牌。
expires_in:过期时间(秒)
scope,令牌的权限范围,服务端可以根据令牌的权限范围去对令牌授权。
jti:令牌的唯一标识。
校验JWT令牌
###校验jwt令牌
POST http://localhost:63070/auth/oauth/check_token?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsieHVlY2hlbmctcGx1cyJdLCJ1c2VyX25hbWUiOiJ7XCJjcmVhdGVUaW1lXCI6XCIyMDIyLTA5LTI4VDA4OjMyOjAzXCIsXCJpZFwiOlwiNDhcIixcIm5hbWVcIjpcIuezu-e7n-euoeeQhuWRmFwiLFwicGVybWlzc2lvbnNcIjpbXSxcInNleFwiOlwiMVwiLFwic3RhdHVzXCI6XCIxXCIsXCJ1c2VybmFtZVwiOlwiYWRtaW5cIixcInV0eXBlXCI6XCIxMDEwMDNcIn0iLCJzY29wZSI6WyJhbGwiXSwiZXhwIjoxNzA4MDAzMzQzLCJhdXRob3JpdGllcyI6WyJ0ZXN0Il0sImp0aSI6IjhlMmQ4YzFhLTliMWUtNDQyNS05M2M2LWRlM2Y0MjBjZjdmNyIsImNsaWVudF9pZCI6IlhjV2ViQXBwIn0.EteHLQ0KfUFG6Etyee3uyCa2RIUi7c6iu496-gh6RCY
{
"aud": [
"xuecheng-plus"
],
"user_name": "{\"createTime\":\"2022-09-28T08:32:03\",\"id\":\"48\",\"name\":\"系统管理员\",\"permissions\":[],\"sex\":\"1\",\"status\":\"1\",\"username\":\"admin\",\"utype\":\"101003\"}",
"scope": [
"all"
],
"active": true,
"exp": 1708003343,
"authorities": [
"test"
],
"jti": "8e2d8c1a-9b1e-4425-93c6-de3f420cf7f7",
"client_id": "XcWebApp"
}
其中username为一个json字符串,返回的是用户对象。
4.2、xuecheng-plus-content服务校验JWT令牌:
在xuecheng-plus-content服务中添加依赖
<!-- 集成安全框架,JWT认证-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
复制TokenConfig和ResouceServerConfig 到内容管理的api工程的config包下。
### 携带token访问资源服务
GET http://localhost:63040/content/content/course/121
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsieHVlY2hlbmctcGx1cyJdLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE3MDc5MjM2NzcsImF1dGhvcml0aWVzIjpbInAxIl0sImp0aSI6IjU4ZjFlMjM5LTYzNjQtNDEyMC1hZjg3LTAxZjk2NWRhYmNjZSIsImNsaWVudF9pZCI6IlhjV2ViQXBwIn0.LpJpjA_oqlq4wmc_ZJ9fEE3gCF37bujIodCALbhO2A4
token正确:
{
"id": 121,
"companyId": 1232141425,
"companyName": null,
"name": "Spring Cloud 开发实战",
"users": "具有web开发基础",
"tags": "",
"mt": "1-3",
"st": "1-3-2",
"grade": "204002",
"teachmode": "200002",
"description": "Spring Cloud是一系列框架的有序集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。Spring Cloud并没有重复制造轮子,它只是将各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。",
"pic": "/mediafiles/2023/02/09/ef29eb93515e32f2d897956d5d914db7.png",
"createDate": "2023-02-09 11:10:42",
"changeDate": null,
"createPeople": null,
"changePeople": null,
"auditStatus": "202004",
"status": "203002",
"charge": "201001",
"price": 1.0,
"originalPrice": 100.0,
"qq": "2323232",
"wechat": "3232432",
"phone": "432432",
"validDays": 365,
"mtName": "Java",
"stName": "编程开发"
}
Response code: 200; Time: 307ms; Content length: 792 bytes
token不正确:
{
"error": "invalid_token",
"error_description": "Cannot convert access token to JSON"
}
4.3、xuecheng-plus-gateway服务中统一认证
网关的职责:
(1)网站白名单维护
针对不用认证的URL全部放行。
(2)校验jwt的合法性。
除了白名单剩下的就是需要认证的请求,网关需要验证jwt的合法性,jwt合法则说明用户身份合法,否则说明身份不合法则拒绝继续访问。
网关不负责授权。
在pom.xml中添加依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
拷贝课程资料下网关认证配置类到网关工程的config包下。(略)
配置白名单文件security-whitelist.properties
/auth/**=认证地址
/content/open/**=内容管理公开访问接口
/media/open/**=媒资管理公开访问接口
### 通过网关访问资源服务
GET http://localhost:63010/content/content/course/121
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsieHVlY2hlbmctcGx1cyJdLCJ1c2VyX25hbWUiOiJ7XCJjcmVhdGVUaW1lXCI6XCIyMDIyLTA5LTI4VDA4OjMyOjAzXCIsXCJpZFwiOlwiNDhcIixcIm5hbWVcIjpcIuezu-e7n-euoeeQhuWRmFwiLFwic2V4XCI6XCIxXCIsXCJzdGF0dXNcIjpcIjFcIixcInVzZXJuYW1lXCI6XCJhZG1pblwiLFwidXR5cGVcIjpcIjEwMTAwM1wifSIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE3MDc5NzQxNjQsImF1dGhvcml0aWVzIjpbInRlc3QiXSwianRpIjoiMzFiNzI2MDQtMmU3Ny00M2RkLTkzYTYtY2ExYjAwMzJkN2ExIiwiY2xpZW50X2lkIjoiWGNXZWJBcHAifQ.h5AkBNwnWSIyfZ8A5GGxTfB9PPw_p737cIAZCYP4AJE
因为网关工程已经统一进行校验,所以在其他微服务的ResouceServerConfig类的public void configure(HttpSecurity http)方法,注释.antMatchers("/r/**","/course/**").authenticated()。
用户认证
1、用户名-密码模式、微信登录模式
我们需要在认证服务中连接自己的数据库,从数据库中读取用户信息进行认证。
spring-security的执行链:
提交账号和密码由DaoAuthenticationProvider调用UserDetailsService的loadUserByUsername()方法获取UserDetails用户信息。
此时需要自定义一个类实现UserDetailsService,返回UserDetails。
完整代码:
/**
* @ClassName : UserDetailsServiceImpl
* @Description : 重写spring-security的UserDetailsService
* @Author : abc123
* @Date: 2024-02-15 10:55
*/
@Component
@Slf4j
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private XcUserMapper userMapper;
@Autowired
private ApplicationContext applicationContext;
/**
* Locates the user based on the username. In the actual implementation, the search
* may possibly be case sensitive, or case insensitive depending on how the
* implementation instance is configured. In this case, the <code>UserDetails</code>
* object that comes back may have a username that is of a different case than what
* was actually requested..
*
* @param username the username identifying the user whose data is required.
* @return a fully populated user record (never <code>null</code>)
* @throws UsernameNotFoundException if the user could not be found or the user has no
* GrantedAuthority
*
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//扩展后的userName是一个JSON字符串,类型是AuthParamsDto
AuthParamsDto authParamsDto = null;
try {
authParamsDto = JSON.parseObject(username,AuthParamsDto.class);
} catch (Exception e) {
log.error("authParamsDto类型转换错误!");
return null;
}
//从dto中取出用户登录类型
String authType = authParamsDto.getAuthType();
//从spring上下文取出对应的bean
AuthService authService = applicationContext.getBean(authType + "_authservice", AuthService.class);
//执行校验方式
XcUserExt user = authService.execute(authParamsDto);
return initUserData(user);
}
private UserDetails initUserData(XcUser user) {
//2.取出数据库存储的正确密码
String password = user.getPassword();
user.setPassword(null);
String userStr = JSON.toJSONString(user);
//3.用户权限,如果不加报Cannot pass a null GrantedAuthority collection
String[] auths = {"test"};
//4.创建UserDetails对象,权限信息待实现授权功能时再向UserDetail中加入
return User.withUsername(userStr).password(password).authorities(auths).build();
}
// public static void main(String[] args) {
// String password = "123456";
// PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
// for(int i=0;i<10;i++) {
// //每个计算出的Hash值都不一样
// String hashPass = passwordEncoder.encode(password);
// System.out.println(hashPass);
// //虽然每次计算的密码Hash值不一样但是校验是通过的
// boolean f = passwordEncoder.matches(password, hashPass);
// System.out.println(f);
// }
// }
}
说明:
扩展点1:由于UserDetails接口只返回了username、密码等信息,如想要在jwt令牌中存储用户的昵称、头像、qq等信息,需要对参数进行扩展。
对入参进行修改,原密码模式:
### 密码模式
POST http://localhost:63070/auth/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username=admin&password=123456
扩展后的密码模式:
################扩展认证请求参数后######################
###密码模式+验证码
POST http://localhost:63070/auth/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username={"username":"admin","authType":"password","password":"123456","checkcodekey": "checkcode:f80334ff0af845298cfff786dae27d82","checkcode":"JN2N"}
传入的userName是一个json字符串。
扩展点2:由于本项目后续会采用微信登录,所以创建AuthParamsDto类。
/**
* @author Mr.M
* @version 1.0
* @description 认证用户请求参数
* @date 2022/9/29 10:56
*/
@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
}
重点:authType认证类型,后续用于分辨采取何种方式登录。:
创建一个统一认证接口模板
/**
* @ClassName : AuthService
* @Description : 统一认证接口
* @Author : abc123
* @Date: 2024-02-15 14:39
*/
public interface AuthService {
XcUserExt execute(AuthParamsDto dto);
}
密码登录实现类:
/**
* @ClassName : PasswordAuthServiceImpl
* @Description : 密码登录
* @Author : abc123
* @Date: 2024-02-15 14:40
*/
@Service("password_authservice")
@Slf4j
public class PasswordAuthServiceImpl implements AuthService{
@Autowired
private XcUserMapper userMapper;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private CheckCodeClient checkCodeClient;
@Override
public XcUserExt execute(AuthParamsDto dto) {
String username = dto.getUsername();
//1.根据用户名查询用户
XcUser user = null;
try {
user = userMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, dto.getUsername()));
} catch (Exception e) {
log.error("查询用户错误,用户名:{},原因:{}",username,e.getMessage());
return null;
}
if (ObjectUtils.isEmpty(user)){
log.info("用户:{}不存在!",username);
return null;
}
//校验密码
String inputPassword = dto.getPassword();
String password = user.getPassword();
boolean matches = passwordEncoder.matches(inputPassword, password);
if (!matches){
log.info("{}:账户密码错误!",username);
return null;
}
//远程调用checkCode服务,校验验证码
Boolean result = checkCodeClient.verify(dto.getCheckcodekey(), dto.getCheckcode());
if (!result){
log.info("{}:验证码错误",username);
}
XcUserExt xcUserExt = new XcUserExt();
BeanUtils.copyProperties(user,xcUserExt);
return xcUserExt;
}
}
同时为了让security框架使用自己定义的验证密码方式,需要重写DaoAuthenticationProviderCustom类
/**
* @ClassName : DaoAuthenticationProviderCustom
* @Description : 重写spring-security的DaoAuthenticationProviderCustom
* @Author : abc123
* @Date: 2024-02-15 12:00
*/
@Component
@Slf4j
public class DaoAuthenticationProviderCustom extends DaoAuthenticationProvider {
@Autowired
public void setUserDetailsService(UserDetailsService userDetailsService) {
super.setUserDetailsService(userDetailsService);
}
/**
* 空方法覆盖父类DaoAuthenticationProvider中的密码验证。
* @param userDetails
* @param authentication
* @throws AuthenticationException
*/
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
}
}
Q1:在这个类中,重写了父类的additionalAuthenticationChecks方法,为什么会覆盖父类的additionalAuthenticationChecks?
在Java中,子类可以重写(覆盖)父类的方法,这是面向对象编程的一个基本特性之一。当子类中定义了一个与父类中具有相同名称和参数列表的方法时,就会发生方法的重写。
在Java中,方法重写的原则是:
- 子类方法的访问修饰符不能低于父类方法的访问修饰符。
- 子类方法不能抛出比父类方法更广泛的异常,或者可以抛出更少的异常。
- 子类方法的返回类型必须是与父类方法相同或是其子类。
Q2:方法注入和字段注入的区别?
-
@Autowired 方法注入:
@Autowired
注解标记在方法上,Spring 在初始化 Bean 时会自动调用这个方法,并将指定类型的 Bean 实例注入到方法的参数中。- 在方法内部可以通过参数来访问被注入的 Bean 实例。
- 这种方式适用于需要在 Bean 初始化时执行一些额外的操作,例如将注入的 Bean 实例传递给父类的方法。
-
@Autowired 字段注入:
@Autowired
注解也可以标记在字段上,Spring 在初始化 Bean 时会自动将对应类型的 Bean 实例注入到标记了@Autowired
的字段中。- 使用字段注入时,被注入的 Bean 实例可以直接通过字段名访问,而不需要额外的方法来设置。
- 这种方式通常用于简单的依赖注入场景,例如直接将一个依赖的 Bean 注入到类的字段中使用。
Q3:UserDetailsService有很多实现类,为什么注入的就是自定义的UserDetailsServiceImpl?
DaoAuthenticationProvider类:
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
...
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
...
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
protected UserDetailsService getUserDetailsService() {
return userDetailsService;
}
Spring 在注入依赖时会优先选择标记了 @Component
或其他类似的注解的 Bean。如果有多个实现了同一个接口的类,并且其中某些类被标记为了 Spring 管理的组件(比如标记了 @Component
、@Service
、@Repository
等注解),而另一些没有被标记,那么 Spring 会优先选择被标记的类作为注入的对象。
微信登录实现类:
/**
* @ClassName : WxAuthServiceImpl
* @Description : 微信登录
* @Author : abc123
* @Date: 2024-02-15 14:42
*/
@Slf4j
@Service("sms_authservice")
public class WxAuthServiceImpl implements AuthService,WxLoginService{
@Autowired
private XcUserMapper userMapper;
@Autowired
private XcUserRoleMapper xcUserRoleMapper;
@Value("${weixin.appid}")
private String appid;
@Value("${weixin.secret}")
private String secret;
@Autowired
private RestTemplate restTemplate;
@Override
public XcUserExt execute(AuthParamsDto dto) {
String username = dto.getUsername();
XcUser user = userMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, username));
if (ObjectUtils.isEmpty(user)){
log.error("{}:该用户不存在!",username);
return null;
}
XcUserExt xcUserExt = new XcUserExt();
BeanUtils.copyProperties(user,xcUserExt);
return xcUserExt;
}
/**
* 微信登录处理
* @param code
* @param state
*/
@Override
public XcUser loginHandle(String code, String state) {
//通过code获取access_token
Map<String,String> resultMap = this.getAccessToken(code);
if (resultMap.isEmpty()){
log.error("通过code获取access_token为空!");
return null;
}
String access_token = resultMap.get("access_token");
String openid = resultMap.get("openid");
//获取用户信息
Map<String, String> userInfoMap = this.getUserInfo(access_token, openid);
if (userInfoMap.isEmpty()){
log.error("通过code获取用户信息为空!");
return null;
}
//保存用户信息
return this.initUserDBData(userInfoMap);
}
@Transactional
public XcUser initUserDBData(Map<String, String> userInfoMap) {
String openid = userInfoMap.get("openid");
//根据unionid查询数据库
XcUser xcUser = userMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getWxUnionid, openid));
if(xcUser!=null){
log.error("{}:该用户已存在!",openid);
return xcUser;
}
String userId = UUID.randomUUID().toString();
xcUser = new XcUser();
xcUser.setId(userId);
xcUser.setWxUnionid(openid);
//记录从微信得到的昵称
xcUser.setNickname(userInfoMap.get("nickname").toString());
xcUser.setUserpic(userInfoMap.get("headimgurl").toString());
xcUser.setName(userInfoMap.get("nickname").toString());
xcUser.setUsername(openid);
xcUser.setPassword(openid);
xcUser.setUtype("101001");//学生类型
xcUser.setStatus("1");//用户状态
xcUser.setCreateTime(LocalDateTime.now());
userMapper.insert(xcUser);
//初始化角色
XcUserRole xcUserRole = new XcUserRole();
xcUserRole.setId(UUID.randomUUID().toString());
xcUserRole.setUserId(userId);
xcUserRole.setRoleId("17");//学生角色
xcUserRoleMapper.insert(xcUserRole);
return xcUser;
}
/**
* 获取用户信息
* 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 url = "https://api.weixin.qq.com/sns/userinfo?access_token="+access_token+"&openid="+openid;
log.info("调用微信接口获取用户信息, url:{}", url);
ResponseEntity<String> exchange = restTemplate.exchange(url, HttpMethod.GET, null, String.class);
if (ObjectUtils.isEmpty(exchange)){
log.error("调用微信接口获取用户信息异常: 返回为空!");
return null;
}
String result = new String(exchange.getBody().getBytes(StandardCharsets.ISO_8859_1),StandardCharsets.UTF_8);
log.info("调用微信接口获取用户信息: 返回值:{}", result);
return JSON.parseObject(result, Map.class);
}
/**
* 通过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
* {
* "access_token":"ACCESS_TOKEN",
* "expires_in":7200,
* "refresh_token":"REFRESH_TOKEN",
* "openid":"OPENID",
* "scope":"SCOPE",
* "unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
* }
*/
private Map<String, String> getAccessToken(String code) {
String url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid="+appid+"&secret="+secret+"&code="+code+"&grant_type=authorization_code";
log.info("调用微信接口申请access_token, url:{}", url);
ResponseEntity<String> exchange = restTemplate.exchange(url, HttpMethod.POST, null, String.class);
if (ObjectUtils.isEmpty(exchange)){
log.error("调用微信接口申请access_token异常: 返回为空!");
return null;
}
String result = exchange.getBody();
log.info("调用微信接口申请access_token: 返回值:{}", result);
return JSON.parseObject(result, Map.class);
}
}
这里在声明时,@Service注解指定了名称,前缀为AuthParamsDto类-authType
根据获取到的authType,拼接上后缀,得到对应的bean容器,执行其中的方法。
//从dto中取出用户登录类型
String authType = authParamsDto.getAuthType();
//从spring上下文取出对应的bean
AuthService authService = applicationContext.getBean(authType + "_authservice", AuthService.class);
//执行校验方式
XcUserExt user = authService.execute(authParamsDto);
封装获取当前用户身份工具类:
/**
* @ClassName : GetLoginInfo
* @Description : 获取当前登录人信息工具类
* @Author : abc123
* @Date: 2024-02-15 11:17
*/
@Slf4j
public class GetLoginInfo {
public static XcUser getLoginInfo(){
//取出当前用户身份
try {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (ObjectUtils.isEmpty(principal)){
return null;
}
if (principal instanceof String){
String userStr = principal.toString();
//反序列化
XcUser xcUser = JSON.parseObject(userStr, XcUser.class);
return xcUser;
}
} catch (Exception e) {
e.printStackTrace();
log.error("获取当前登录人信息出错");
}
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;
}
}
@ApiOperation("根据id查询课程信息")
@GetMapping("/content/course/{courseId}")
public CourseBaseInfoDto getCourseInfoById(@PathVariable("courseId") Long courseId){
//取出当前用户身份
// Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
GetLoginInfo.XcUser user = GetLoginInfo.getLoginInfo();
log.info("用户名:{},id:{}",user.getName(),user.getId());
return courseBaseService.getCourseInfoById(courseId);
}
2、用户名-密码整合验证码模式
2.1、搭建xuecheng-plus-checkcode服务
nacos配置-xuecheng-plus-checkcode:
nacos配置-xuecheng-plus-redis:
本项目采用的验证码模式:
(1)先生成一个指定位数的验证码,根据需要可能是数字、数字字母组合或文字。
(2)根据生成的验证码生成一个图片并返回给页面
(3)给生成的验证码分配一个key,将key和验证码一同存入缓存。这个key和图片一同返回给页面。
(4)用户输入验证码,连同key一同提交至认证服务。
(5)认证服务拿key和输入的验证码请求验证码服务去校验
(6)验证码服务根据key从缓存取出正确的验证码和用户输入的验证码进行比对,如果相同则校验通过,否则不通过。
### 申请验证码
POST localhost:63075/checkcode/pic
{
"key": "checkcode:5bca5bd5fe8f41ddbc5596110baa490b",
"aliasing": "data:image/png;base64,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"
}
Response code: 200; Time: 95ms; Content length: 4080 bytes
可复制aliasing中的base64字符串粘贴至浏览器查看验证码图片。
2.2、远程调用xuecheng-plus-checkcode服务整合验证码
xuecheng-plus-auth中定义feign接口
/**
* @ClassName : CheckCodeClient
* @Description : 远程调用验证码服务
* @Author : abc123
* @Date: 2024-02-15 15:27
*/
@FeignClient("checkcode")
@RequestMapping("/checkcode")
public interface CheckCodeClient {
/**
* 校验验证码
* @param key 验证key
* @param code 验证码
* @return true 验证通过 false 验证不通过
*/
@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")
Boolean verify(@RequestParam("key") String key, @RequestParam("code") String code);
}
Q1:为什么参数上要加@RequestParam注解?
为了将参数传递给远程服务的 verify
方法,需要使用 @RequestParam("key")
注解来标识参数的名称。这是因为 Feign 默认情况下将参数作为请求体(Body)中的参数进行传递,而不是作为查询参数(Query Parameter)。但是在接口中,key
参数是作为查询参数传递的,因此需要显式地使用 @RequestParam
注解来标识参数的名称。
如果不加 @RequestParam("key")
注解,Feign 在进行远程调用时会将参数作为请求体(Body)中的参数进行传递,而不是作为查询参数(Query Parameter)。这意味着参数 key
的值会被包含在请求的请求体中,而不是作为查询参数附加到请求的 URL 中。
/**
* @ClassName : PasswordAuthServiceImpl
* @Description : 密码登录
* @Author : abc123
* @Date: 2024-02-15 14:40
*/
@Service("password_authservice")
@Slf4j
public class PasswordAuthServiceImpl implements AuthService{
@Autowired
private XcUserMapper userMapper;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private CheckCodeClient checkCodeClient;
@Override
public XcUserExt execute(AuthParamsDto dto) {
...
//远程调用checkCode服务,校验验证码
Boolean result = checkCodeClient.verify(dto.getCheckcodekey(), dto.getCheckcode());
...
}
}