学成在线day10 JWT 用户认证

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中,方法重写的原则是:

  1. 子类方法的访问修饰符不能低于父类方法的访问修饰符。
  2. 子类方法不能抛出比父类方法更广泛的异常,或者可以抛出更少的异常。
  3. 子类方法的返回类型必须是与父类方法相同或是其子类。

Q2:方法注入和字段注入的区别?

  1. @Autowired 方法注入

    • @Autowired 注解标记在方法上,Spring 在初始化 Bean 时会自动调用这个方法,并将指定类型的 Bean 实例注入到方法的参数中。
    • 在方法内部可以通过参数来访问被注入的 Bean 实例。
    • 这种方式适用于需要在 Bean 初始化时执行一些额外的操作,例如将注入的 Bean 实例传递给父类的方法。
  2. @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());
        ...
        
    }
}

  • 15
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值