用户认证(含微信扫码登录和用户名密码登录)

用户认证

需求分析

至此我们了解了使用Spring Security进行认证授权的过程,本节实现用户认证功能。

目前各大网站的认证方式非常丰富:账号密码认证、手机验证码认证、扫码登录等。

本项目也要支持多种认证试。

连接数据库认证

基于的认证流程在研究Spring Security过程中已经测试通过,到目前为止用户认证流程如下:

 

认证所需要的用户信息存储在用户中心数据库,现在需要将认证服务连接数据库查询用户信息。

在研究Spring Security的过程中是将用户信息硬编码,如下:

@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;
}

我们要认证服务中连接用户中心数据库查询用户信息。

如何使用Spring Security连接数据库认证吗?

前边学习Spring Security工作原理时有一张执行流程图,如下图:

 

用户提交账号和密码由DaoAuthenticationProvider调用UserDetailsService的loadUserByUsername()方法获取UserDetails用户信息。

查询DaoAuthenticationProvider的源代码如下:

 UserDetailsService是一个接口,如下:

package org.springframework.security.core.userdetails;

public interface UserDetailsService {
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}

UserDetails是用户信息接口

public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();

    boolean isAccountNonExpired();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();
}

我们只要实现UserDetailsService 接口查询数据库得到用户信息返回UserDetails 类型的用户信息即可,框架调用loadUserByUsername()方法拿到用户信息之后是如何执行的,见下图: 

首先屏蔽原来定义的UserDetailsService。 (在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;
//    }

下边自定义UserDetailsService

 实现从数据库查询客户数据

/**
 * @author Mr.M
 * @version 1.0
 * @description TODO
 * @date 2022/9/28 18:09
 */
@Service
public class UserServiceImpl implements UserDetailsService {

    @Autowired
    XcUserMapper xcUserMapper;

    /**
     * @description 根据账号查询用户信息
     * @param s  账号
     * @return org.springframework.security.core.userdetails.UserDetails
     * @author Mr.M
     * @date 2022/9/28 18:30
    */
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {

        XcUser user = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, s));
        if(user==null){
            //返回空表示用户不存在
            return null;
        }
        //取出数据库存储的正确密码
        String password  =user.getPassword();
        //用户权限,如果不加报Cannot pass a null GrantedAuthority collection
        String[] authorities= {"test"};
        //创建UserDetails对象,权限信息待实现授权功能时再向UserDetail中加入
        UserDetails userDetails = User.withUsername(user.getUsername()).password(password).authorities(authorities).build();

        return userDetails;
    }


}

 数据库中的密码加过密的,用户输入的密码是明文,我们需要修改密码格式器PasswordEncoder,原来使用的是NoOpPasswordEncoder,它是通过明文方式比较密码,现在我们修改为BCryptPasswordEncoder,它是将用户输入的密码编码为BCrypt格式与数据库中的密码进行比对。

将在WebSecurityConfig配置的密码格式器修改

 @Bean
    public PasswordEncoder passwordEncoder() {
//        //密码为明文方式
//        return NoOpPasswordEncoder.getInstance();
        //密码加密后
        return new BCryptPasswordEncoder();
    }

我们通过测试代码测试BCryptPasswordEncoder,如下:

public static void main(String[] args) {
    String password = "111111";
    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);
    }
}

在 Spring Security 的 BCryptPasswordEncoder 中,每次调用 encode 方法时,它都会生成一个新的随机盐值(salt)并使用该盐值对密码进行哈希处理。因此,每次为相同的密码生成的哈希值(hash)都会不同,因为每个哈希值都包含了不同的随机盐值。

然而,BCryptPasswordEncoder 的 matches 方法在验证密码时考虑了这一点。它不仅仅是比较两个哈希值是否相等,而是提取出哈希值中的盐值,并使用该盐值对提供的原始密码进行哈希处理,然后比较这个新生成的哈希值和存储的哈希值是否匹配。

修改数据库中的密码为Bcrypt格式,并且记录明文密码,稍后申请令牌时需要。

由于修改密码编码方式还需要将客户端的密钥更改为Bcrypt格式.

 在AuthenticationManager对客户端秘钥进行修改

 //客户端详情服务
  @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")
   ;
  }

现在重启认证服务。

下边使用httpclient进行测试:

### 密码模式
POST {{auth_host}}/auth/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username=stu1&password=111111

输入正确的账号和密码,申请令牌成功。

输入错误的密码,报错:

Java
{
  "error": "invalid_grant",
  "error_description": "用户名或密码错误"
}

输入错误的账号,报错:

Java
{
  "error": "unauthorized",
  "error_description": "UserDetailsService returned null, which is an interface contract violation"
}

扩展用户身份信息

 用户表中存储了用户的账号、手机号、email,昵称、qq等信息,UserDetails接口只返回了username、密码等信息,如下:

public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();

    boolean isAccountNonExpired();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();
}

我们需要扩展用户身份的信息,在jwt令牌中存储用户的昵称、头像、qq等信息。

如何扩展Spring Security的用户身份信息呢?

在认证阶段DaoAuthenticationProvider会调用UserDetailService查询用户的信息,这里是可以获取到齐全的用户信息的。由于JWT令牌中用户身份信息来源于UserDetails,UserDetails中仅定义了username为用户的身份信息,这里有两个思路:第一是可以扩展UserDetails,使之包括更多的自定义属性,第二也可以扩展username的内容 ,比如存入json数据内容作为username的内容。相比较而言,方案二比较简单还不用破坏UserDetails的结构,我们采用方案二。

修改UserServiceImpl如下:

@Service
public class UserServiceImpl implements UserDetailsService {

    @Autowired
    XcUserMapper xcUserMapper;

    /**
     * @description 根据账号查询用户信息
     * @param s  账号
     * @return org.springframework.security.core.userdetails.UserDetails
     * @author Mr.M
     * @date 2022/9/28 18:30
     */
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {

        XcUser user = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, s));
        if(user==null){
            //返回空表示用户不存在
            return null;
        }
        //取出数据库存储的正确密码
        String password  =user.getPassword();
        //用户权限,如果不加报Cannot pass a null GrantedAuthority collection
        String[] authorities= {"test"};
        //创建UserDetails对象,权限信息待实现授权功能时再向UserDetail中加入
        //令牌最终存的内容为userDetails
        //为了安全在令牌中不放密码
        user.setPassword(null);
        //将user对象转json
        String userString = JSON.toJSONString(user);
        //创建UserDetails对象
        UserDetails userDetails = User.withUsername(userString).password(password).authorities(authorities).build();

        return userDetails;
    }


}

重启认证服务,重新生成令牌,生成成功。

我们可以使用check_token查询jwt的内容

JSON
###校验jwt令牌
POST {{auth_host}}/oauth/check_token?token=

响应示例如下,

JSON

{
  "aud": [
    "res1"
  ],
  "user_name": "{\"birthday\":\"2022-09-28T19:28:46\",\"createTime\":\"2022-09-28T08:32:03\",\"id\":\"50\",\"name\":\"学生1\",\"nickname\":\"大水牛\",\"password\":\"$2a$10$0pt7WlfTbnPDTcWtp/.2Mu5CTXvohnNQhR628qq4RoKSc0dGAdEgm\",\"sex\":\"1\",\"status\":\"1\",\"username\":\"stu1\",\"userpic\":\"http://file.51xuecheng.cn/dddf\",\"utype\":\"101001\"}",
  "scope": [
    "all"
  ],
  "active": true,
  "exp": 1664372184,
  "authorities": [
    "p1"
  ],
  "jti": "73da9f7b-bd8c-45ac-9add-46b711d11fb8",
  "client_id": "c1"
}

user_name存储了用户信息的json格式,在资源服务中就可以取出该json格式的内容转为用户对象去使用。

资源服务获取用户身份

下边编写一个工具类在各个微服务中去使用,获取当前登录用户的对象。

在content-api中定义此类:



/**
 * @author Mr.M
 * @version 1.0
 * @description 获取当前用户身份工具类
 * @date 2022/10/18 18:02
 */
@Slf4j
public class SecurityUtil {

    public static XcUser getUser() {
        try {
            Object principalObj = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
            if (principalObj instanceof String) {
                //取出用户身份信息
                String principal = principalObj.toString();
                //将json转成对象
                XcUser user = JSON.parseObject(principal, XcUser.class);
                return user;
            }
        } catch (Exception e) {
            log.error("获取当前登录用户身份出错:{}", e.getMessage());
            e.printStackTrace();
        }

        return null;
    }


    @Data
    public static class XcUser implements Serializable {

        private static final long serialVersionUID = 1L;

        private String id;

        private String username;

        private String password;

        private String salt;

        private String name;
        private String nickname;
        private String wxUnionid;
        private String companyId;
        /**
         * 头像
         */
        private String userpic;

        private String utype;

        private LocalDateTime birthday;

        private String sex;

        private String email;

        private String cellphone;

        private String qq;

        /**
         * 用户状态
         */
        private String status;

        private LocalDateTime createTime;

        private LocalDateTime updateTime;


    }


}

 下边在内容管理服务中测试此工具类,以查询课程信息接口为例:

@ApiOperation("根据课程id查询课程基础信息")
@GetMapping("/course/{courseId}")
public CourseBaseInfoDto getCourseBaseById(@PathVariable("courseId") Long courseId){
    //取出当前用户身份
//    Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
   SecurityUtil.XcUser user = SecurityUtil.getUser();
    System.out.println(user);

    return courseBaseInfoService.getCourseBaseInfo(courseId);
}

重启内容管理服务:

1、启动认证服务、网关、内容管理服务

2、生成新的令牌

3、携带令牌访问内容管理服务的查询课程接口

支持认证方式多样 

统一认证入口 

目前各大网站的认证方式非常丰富:账号密码认证、手机验证码认证、扫码登录等。基于当前研究的Spring Security认证流程如何支持多样化的认证方式呢?

1、支持账号和密码认证

采用OAuth2协议的密码模式即可实现。

2、支持手机号加验证码认证

用户认证提交的是手机号和验证码,并不是账号和密码。

3、微信扫码认证

基于OAuth2协议与微信交互,学成在线网站向微信服务器申请到一个令牌,然后携带令牌去微信查询用户信息,查询成功则用户在学成在线项目认证通过。

目前我们测试通过OAuth2的密码模式,用户认证会提交账号和密码,由DaoAuthenticationProvider调用UserDetailsService的loadUserByUsername()方法获取UserDetails用户信息。

在前边我们自定义了UserDetailsService接口实现类,通过loadUserByUsername()方法根据账号查询用户信息。

而不同的认证方式提交的数据不一样,比如:手机加验证码方式会提交手机号和验证码,账号密码方式会提交账号、密码、验证码。

我们可以在loadUserByUsername()方法上作文章,将用户原来提交的账号数据改为提交json数据,json数据可以扩展不同认证方式所提交的各种参数。

首先创建一个DTO类表示认证的参数:

/**
 * @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


}

 此时loadUserByUsername()方法可以修改如下:

/**
 * @author Mr.M
 * @version 1.0
 * @description TODO
 * @date 2022/9/28 18:09
 */
@Service
@Slf4j
public class UserServiceImpl implements UserDetailsService {

    @Autowired
    XcUserMapper xcUserMapper;

    /**
     * @description 查询用户信息组成用户身份信息
     * @param s  AuthParamsDto类型的json数据
     * @return org.springframework.security.core.userdetails.UserDetails
     * @author Mr.M
     * @date 2022/9/28 18:30
     */
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {

        AuthParamsDto authParamsDto = null;
        try {
            //将认证参数转为AuthParamsDto类型
            authParamsDto = JSON.parseObject(s, AuthParamsDto.class);
        } catch (Exception e) {
            log.info("认证请求不符合项目要求:{}",s);
            throw new RuntimeException("认证请求数据格式不对");
        }
        //账号
        String username = authParamsDto.getUsername();
        XcUser user = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, username));
        if(user==null){
            //返回空表示用户不存在
            return null;
        }
        //取出数据库存储的正确密码
        String password  =user.getPassword();
        //用户权限,如果不加报Cannot pass a null GrantedAuthority collection
        String[] authorities = {"p1"};
        //将user对象转json
        String userString = JSON.toJSONString(user);
        //创建UserDetails对象
        UserDetails userDetails = User.withUsername(userString).password(password).authorities(authorities).build();

        return userDetails;
    }


}

原来的DaoAuthenticationProvider 会进行密码校验,现在重新定义DaoAuthenticationProviderCustom类,重写类的additionalAuthenticationChecks方法。 

在config下重写additionalAuthenticationChecks

/**
 * @description 自定义DaoAuthenticationProvider
 * @author Mr.M
 * @date 2022/9/29 10:31
 * @version 1.0
 */
@Slf4j
@Component
public class DaoAuthenticationProviderCustom extends DaoAuthenticationProvider {


    @Autowired
    public void setUserDetailsService(UserDetailsService userDetailsService) {
        super.setUserDetailsService(userDetailsService);
    }


    //屏蔽密码对比
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication)  {


    }

}

 修改WebSecurityConfig类指定daoAuthenticationProviderCustom

添加一个configure

@Autowired
DaoAuthenticationProviderCustom daoAuthenticationProviderCustom;


@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.authenticationProvider(daoAuthenticationProviderCustom);
}

此时可以重启认证服务,测试申请令牌接口,传入的账号信息改为json数据,如下:

################扩展认证请求参数后######################
###密码模式
POST {{auth_host}}/auth/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username={"username":"stu1","authType":"password","password":"111111"}

经过测试发现loadUserByUsername()方法可以正常接收到认证请求中的json数据。

有了这些认证参数我们可以定义一个认证Service接口去进行各种方式的认证。

定义或拓展用户信息,为了扩展性让它继承XcUser

@Data
public class XcUserExt extends XcUser {
}

定义认证Service 接口

/**
 * @description 认证service
 * @author Mr.M
 * @date 2022/9/29 12:10
 * @version 1.0
 */
public interface AuthService {

   /**
    * @description 认证方法
    * @param authParamsDto 认证参数
    * @return com.xuecheng.ucenter.model.po.XcUser 用户信息
    * @author Mr.M
    * @date 2022/9/29 12:11
   */
   XcUserExt execute(AuthParamsDto authParamsDto);

}

对UserServiceImpl下的loadUserByUsername()进行修改

@Slf4j
@Service
public class UserServiceImpl implements UserDetailsService {

    @Autowired
    XcUserMapper xcUserMapper;

    @Autowired
    ApplicationContext applicationContext;


    /**
     * @description 查询用户信息组成用户身份信息
     * @param s  AuthParamsDto类型的json数据
     * @return org.springframework.security.core.userdetails.UserDetails
     * @author Mr.M
     * @date 2022/9/28 18:30
    */
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {

        AuthParamsDto authParamsDto = null;
        try {
            //将认证参数转为AuthParamsDto类型
            authParamsDto = JSON.parseObject(s, AuthParamsDto.class);
        } catch (Exception e) {
            log.info("认证请求不符合项目要求:{}",s);
            throw new RuntimeException("认证请求数据格式不对");
        }
        //开始认证
        //不同的登录模式对应不同的实现类(具体更改等不同的登录模式的实现类完成再进行)
        authService.execute(authParamsDto);
        .....

 到此我们基于Spring Security认证流程修改为如下:

实现账号密码认证 

上面定义了AuthService认证接口,下边实现该接口实现账号密码认证

/**
 * @description 账号密码认证
 * @author Mr.M
 * @date 2022/9/29 12:12
 * @version 1.0
 */
@Service("password_authservice")
public class PasswordAuthServiceImpl implements AuthService {

    @Autowired
    XcUserMapper xcUserMapper;

    @Autowired
    PasswordEncoder passwordEncoder;


    @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);
        //校验密码
        //取出数据库存储的正确密码
        String passwordDb  =user.getPassword();
        String passwordForm = authParamsDto.getPassword();
        boolean matches = passwordEncoder.matches(passwordForm, passwordDb);
        if(!matches){
            throw new RuntimeException("账号或密码错误");
        }
        return xcUserExt;
    }
}

修改UserServiceImpl类,根据认证方式使用不同的认证bean 

import com.alibaba.fastjson.JSON;
import com.xuecheng.ucenter.mapper.XcUserMapper;
import com.xuecheng.ucenter.model.dto.AuthParamsDto;
import com.xuecheng.ucenter.model.dto.XcUserExt;
import com.xuecheng.ucenter.service.AuthService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

/**
 * @author Mr.M
 * @version 1.0
 * @description TODO
 * @date 2022/9/28 18:09
 */
@Service
@Slf4j
public class UserServiceImpl implements UserDetailsService {

    @Autowired
    XcUserMapper xcUserMapper;
    @Autowired
    ApplicationContext applicationContext; 
    /**
     * @description 查询用户信息组成用户身份信息
     * @param s  AuthParamsDto类型的json数据
     * @return org.springframework.security.core.userdetails.UserDetails
     * @author Mr.M
     * @date 2022/9/28 18:30
     */
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {

        AuthParamsDto authParamsDto = null;
        try {
            //将认证参数转为AuthParamsDto类型
            authParamsDto = JSON.parseObject(s, AuthParamsDto.class);
        } catch (Exception e) {
            log.info("认证请求不符合项目要求:{}",s);
            throw new RuntimeException("认证请求数据格式不对");
        }
        //开始认证
        //认证方法
        String authType = authParamsDto.getAuthType();
        AuthService authService = applicationContext.getBean(authType + "_authservice",AuthService.class);
        XcUserExt user = authService.execute(authParamsDto);

        return getUserPrincipal(user);
    }

    /**
     * @description 查询用户信息
     * @param user  用户id,主键
     * @return com.xuecheng.ucenter.model.po.XcUser 用户信息
     * @author Mr.M
     * @date 2022/9/29 12:19
     */
    public UserDetails getUserPrincipal(XcUserExt user){
        //用户权限,如果不加报Cannot pass a null GrantedAuthority collection
        String[] authorities = {"p1"};
        String password = user.getPassword();
        //为了安全在令牌中不放密码
        user.setPassword(null);
        //将user对象转json
        String userString = JSON.toJSONString(user);
        //创建UserDetails对象
        UserDetails userDetails = User.withUsername(userString).password(password ).authorities(authorities).build();
        return userDetails;
    }


}

重启认证服务,测试申请令牌接口。

1、测试账号和密码都正确的情况是否可以申请令牌成功。

2、测试密码错误的情况。

3、测试账号不存在情况。

验证码服务

在认证时一般都需要输入验证码,验证码有什么用?

验证码可以防止恶性攻击,比如:XSS跨站脚本攻击、CSRF跨站请求伪造攻击,一些比较复杂的图形验证码可以有效的防止恶性攻击。

为了保护系统的安全在一些比较重要的操作都需要验证码。

验证码的类型也有很多:图片、语音、手机短信验证码等。

本项目创建单独的验证码服务为各业务提供验证码的生成、校验等服务。

拷贝课程资料目录xuecheng-plus-checkcode验证码服务工程到自己的工程目录。

 定义nacos配置文件

server:
  servlet:
    context-path: /checkcode
  port: 63075

注意修改bootstrap.yml中的命名空间为自己定义的命名空间。

配置redis-dev.yaml,

内容如下:

XML
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

 验证码接口测试

验证码服务对外提供的接口有:

1、生成验证码

2、校验验证码。

如下:

验证码服务如何生成并校验验证码?

拿图片验证码举例:

1、先生成一个指定位数的验证码,根据需要可能是数字、数字字母组合或文字。

2、根据生成的验证码生成一个图片并返回给页面

3、给生成的验证码分配一个key,将key和验证码一同存入缓存。这个key和图片一同返回给页面。

4、用户输入验证码,连同key一同提交至认证服务。

5、认证服务拿key和输入的验证码请求验证码服务去校验

6、验证码服务根据key从缓存取出正确的验证码和用户输入的验证码进行比对,如果相同则校验通过,否则不通过。

 根据接口分析,验证码服务接口如下:

import com.xuecheng.base.model.RestResponse;
import com.xuecheng.checkcode.model.CheckCodeParamsDto;
import com.xuecheng.checkcode.model.CheckCodeResultDto;
import com.xuecheng.checkcode.service.CheckCodeService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.util.Map;

/**
 * @author Mr.M
 * @version 1.0
 * @description 验证码服务接口
 * @date 2022/9/29 18:39
 */
@Api(value = "验证码服务接口")
@RestController
public class CheckCodeController {


    @ApiOperation(value="生成验证信息", notes="生成验证信息")
    @PostMapping(value = "/pic")
    public CheckCodeResultDto generatePicCheckCode(CheckCodeParamsDto 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){
        
    }
}

1、生成验证码接口

Plain Text
### 申请验证码
POST {{checkcode_host}}/checkcode/pic

{
  "key": "checkcode:cd53f6e0bd8a4553a3ec5643c756bbb6",
  "aliasing": "data:image/png;base64,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"
}

aliasing:为图片 

2、校验验证码接口

根据生成验证码返回的key以及日志中输出正确的验证码去测试。

Bash
### 校验验证码
POST {{checkcode_host}}/checkcode/verify?key=checkcode4506b95bddbe46cdb0d56810b747db1b&code=70dl

账号密码认证 

需求分析

到目前为止账号和密码认证所需要的技术、组件都已开发完毕,下边实现账号密码认证,输出如下图:

执行流程如下:

 账号密码认证开发

1、在认证服务定义远程调用验证码服务的接口 


/**
 * @description 搜索服务远程接口
 * @author Mr.M
 * @date 2022/9/20 20:29
 * @version 1.0
 */
@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);

}

降级方法

@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;
            }
        };
    }
}

启动类要加上注解

@SpringBootApplication
@EnableFeignClients(basePackages={"com.xuecheng.*.feignclient"})
public class AuthApplication {

    public static void main(String[] args) {
        SpringApplication.run(AuthApplication.class, args);
    }

}

 2、完善PasswordAuthServiceImpl

/**
 * @description 账号密码认证
 * @author Mr.M
 * @date 2022/9/29 12:12
 * @version 1.0
 */
@Service("password_authservice")
public class PasswordAuthServiceImpl implements AuthService {

    @Autowired
    XcUserMapper xcUserMapper;

    @Autowired
    PasswordEncoder passwordEncoder;

    @Autowired
    CheckCodeClient checkCodeClient;

    @Override
    public XcUserExt execute(AuthParamsDto authParamsDto) {
        //校验验证码
        String checkcode = authParamsDto.getCheckcode();
        String checkcodekey = authParamsDto.getCheckcodekey();

        if(StringUtils.isBlank(checkcodekey) || StringUtils.isBlank(checkcode)){
            throw new RuntimeException("验证码为空");

        }
        Boolean verify = checkCodeClient.verify(checkcodekey, checkcode);
        if(!verify){
            throw new RuntimeException("验证码输入错误");
        }
        //账号
        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);
        //校验密码
        //取出数据库存储的正确密码
        String passwordDb  =user.getPassword();
        String passwordForm = authParamsDto.getPassword();
        boolean matches = passwordEncoder.matches(passwordForm, passwordDb);
        if(!matches){
            throw new RuntimeException("账号或密码错误");
        }
        return xcUserExt;
    }
}

小技巧:目前账号密码方式添加了验证码校验,为了后期获取令牌方便可以重新定义一个不需要验证码校验的认证类AuthService ,AuthService 中去掉验证码的校验,方便生成令牌。

账号密码认证测试 1、使用浏览器访问 http://www.51xuecheng.cn/sign.html

2、首先测试验证码,分别输入正确的验证码和错误的验证码进行测试

3、输入正确的账号密码和错误的账号密码进行测试

登录成功将jwt令牌存储cookie.

4、测试自动登录

勾选自动登录cookie生成时间为30天,不勾选自动登录关闭浏览器窗口后自动删除cookie。

微信扫码登录 

接入规范

接入流程

微信扫码登录基于OAuth2协议的授权码模式,

接口文档:

https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html

流程如下:

第三方应用获取access_token令牌后即可请求微信获取用户的信息,成功获取到用户的信息表示用户在第三方应用认证成功。 

请求获取授权码

第三方使用网站应用授权登录前请注意已获取相应网页授权作用域(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参数

Plain Text
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):

Plain Text
http://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js

步骤2:在需要使用微信登录的地方实例以下 JS 对象:

Plain Text
 var obj = new WxLogin({
 self_redirect:true,
 id:"login_container",
 appid: "",
 scope: "",
 redirect_uri: "",
  state: "",
 style: "",
 href: ""
 });

通过 code 获取access_token

通过 code 获取access_token

Plain Text
https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code

返回说明

正确的返回:

Plain Text
{
"access_token":"ACCESS_TOKEN",
"expires_in":7200,
"refresh_token":"REFRESH_TOKEN",
"openid":"OPENID",
"scope":"SCOPE",
"unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
}

 参数说明

错误返回样例:

Plain Text
{"errcode":40029,"errmsg":"invalid code"}

4.1.4 通过access_token调用接口

获取access_token后,进行接口调用,有以下前提:

Plain Text
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

接口地址

Plain Text
http请求方式: GET
https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID

如下:

 响应:

说明如下:

Plain Text
参数            说明
openid        普通用户的标识,对当前开发者帐号唯一
nickname        普通用户昵称
sex            普通用户性别,1为男性,2为女性
province        普通用户个人资料填写的省份
city            普通用户个人资料填写的城市
country        国家,如中国为CN
headimgurl        用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空
privilege        用户特权信息,json数组,如微信沃卡用户为(chinaunicom)
unionid          用户统一标识。针对一个微信开放平台帐号下的应用,同一用户的 unionid 是唯一的。

  准备开发环境

 添加应用

1、注册微信开放平台

https://open.weixin.qq.com/

2、添加应用

进入网站应用,添加应用

3、添加应用需要指定一个外网域名作为微信回调域名

审核通过后,生成app密钥。

最终获取appID和AppSecret

内网穿透

我们的开发环境在局域网,微信回调指向一个公网域名。

如何让微信回调请求至我们的开发计算机上呢?

可以使用内网穿透技术,什么是内网穿透?

内网穿透简单来说就是将内网外网通过隧道打通,让内网的数据让外网可以获取。比如常用的办公室软件等,一般在办公室或家里,通过拨号上网,这样办公软件只有在本地的局域网之内才能访问,那么问题来了,如果是手机上,或者公司外地的办公人员,如何访问到办公软件呢?这就需要内网穿透工具了。开启隧道之后,网穿透工具会分配一个专属域名/端口,办公软件就已经在公网上了,在外地的办公人员可以在任何地方愉快的访问办公软件了~~

1、在内网穿透服务器上开通隧道,配置外网域名,配置穿透内网的端口即本地电脑上的端口。

这里我们配置认证服务端口,最终实现通过外网域名访问本地认证服务。

2、在本地电脑上安装内网穿透的工具,工具上配置内网穿透服务器隧道token。

市面上做内网穿透的商家很多,需要时可以查阅资料了解下。(如netapp)

接入微信登录

接入分析

根据OAuth2协议授权码流程,结合本项目自身特点,分析接入微信扫码登录的流程如下:

本项目认证服务需要做哪些事?

1、需要定义接口接收微信下发的授权码。

2、收到授权码调用微信接口申请令牌。

3、申请到令牌调用微信获取用户信息

4、获取用户信息成功将其写入本项目用户中心数据库。

5、最后重定向到浏览器自动登录。

 定义接口

参考接口规范中“请求获取授权码” 定义接收微信下发的授权码接口,

定义WxLoginController类,如下:

@Slf4j
@Controller
public class WxLoginController {

    @RequestMapping("/wxLogin")
    public String wxLogin(String code, String state) throws IOException {
        log.debug("微信扫码回调,code:{},state:{}",code,state);
        //请求微信申请令牌,拿到令牌查询用户信息,将用户信息写入本项目数据库
        XcUser xcUser = new XcUser();
        //暂时硬编写,目的是调试环境
        xcUser.setUsername("t1");
        if(xcUser==null){
            return "redirect:http://www.51xuecheng.cn/error.html";
        }
        String username = xcUser.getUsername();
        return "redirect:http://www.51xuecheng.cn/sign.html?username="+username+"&authType=wx";
    }
}

定义微信认证的service 

/**
 * @author Mr.M
 * @version 1.0
 * @description 微信扫码认证
 * @date 2022/9/29 12:12
 */
@Slf4j
@Service("wx_authservice")
public class WxAuthServiceImpl implements AuthService {

    @Autowired
    XcUserMapper xcUserMapper;

    @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、在/wxLogin接口中打断点

3、打开前端微信扫码页面

点击微信图标打开二维码 

用户扫码,确认授权

此时正常进入 /wxLogin  方法,最后跳转到http://www.51xuecheng.cn/sign.html?username=t1&authType=wx。

申请令牌

接下来请求微信申请令牌。

使用nacos在认证服务的配置文件中配置

weixin:
  appid: wxed9954c01bb89b47
  secret: snsapi_login

1、使用restTemplate请求微信,配置RestTemplate bean

在认证服务的启动类配置restTemplate

 @Bean
    RestTemplate restTemplate(){
        RestTemplate restTemplate = new RestTemplate(new OkHttp3ClientHttpRequestFactory());
        return  restTemplate;
    }

定义与微信认证的service接口:

/**
 * @author Mr.M
 * @version 1.0
 * @description 微信认证接口
 * @date 2023/2/21 22:15
 */
public interface WxAuthService {

    public XcUser wxAuth(String code);

}

 继续在WxAuthServiceImpl类写WxAuthService 接口的实现,在其中定义申请令牌的私有方法并由wxAuth方法去调用:

/**
 * @author Mr.M
 * @version 1.0
 * @description 微信扫码认证
 * @date 2022/9/29 12:12
 */
@Slf4j
@Service("wx_authservice")
public class WxAuthServiceImpl implements AuthService, WxAuthService {

    @Autowired
    XcUserMapper xcUserMapper;
    @Autowired
    RestTemplate restTemplate;
    @Value("${weixin.appid}")
    String appid;
    @Value("${weixin.secret}")
    String secret;
    @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;
    }

    @Override
    public XcUser wxAuth(String code) {
        //收到code调用微信接口申请access_token
        Map<String, String> access_token_map = getAccess_token(code);
        if(access_token_map==null){
            return null;
        }
        //获取用户信息,未完善

        //添加用户到数据库
        XcUser xcUser = null;

        return xcUser;
    }
    /**
     * 申请访问令牌,响应示例
     {
     "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 wxUrl_template = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code";
        //请求微信地址
        String wxUrl = String.format(wxUrl_template, appid, secret, code);

        log.info("调用微信接口申请access_token, url:{}", wxUrl);

        ResponseEntity<String> exchange = restTemplate.exchange(wxUrl, HttpMethod.POST, null, String.class);

        String result = exchange.getBody();
        log.info("调用微信接口申请access_token: 返回值:{}", result);
        Map<String,String> resultMap = JSON.parseObject(result, Map.class);

        return resultMap;
    }
}

下边在controller中调用wxAuth接口(将之前的硬编码替换):

@Slf4j
@Controller
public class WxLoginController {

    @Autowired
    WxAuthService wxAuthService;
    @RequestMapping("/wxLogin")
    public String wxLogin(String code, String state) throws IOException {
        log.debug("微信扫码回调,code:{},state:{}",code,state);
        //请求微信申请令牌,拿到令牌查询用户信息,将用户信息写入本项目数据库
        XcUser xcUser = wxAuthService.wxAuth(code);
        if(xcUser==null){
            return "redirect:http://www.51xuecheng.cn/error.html";
        }
        String username = xcUser.getUsername();
        return "redirect:http://www.51xuecheng.cn/sign.html?username="+username+"&authType=wx";
    }
}

 

测试获取用户信息

1、在wxAuthService中获取用户信息处打断点

2、进入http://www.51xuecheng.cn/wxsign.html

3、手机扫码并授权,观察是否成功申请到令牌

获取用户信息

在WxAuthServiceImpl类中定义获取用户信息方法:

/**获取用户信息,示例如下:
     {
     "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.POST, null, String.class);

        //防止乱码进行转码
        String result = new     String(exchange.getBody().getBytes(StandardCharsets.ISO_8859_1),StandardCharsets.UTF_8);
        log.info("调用微信接口申请access_token: 返回值:{}", result);
        Map<String,String> resultMap = JSON.parseObject(result, Map.class);

        return resultMap;
    }

完善wxAuth

@Override
    public XcUser wxAuth(String code) {
        //收到code调用微信接口申请access_token
        Map<String, String> access_token_map = getAccess_token(code);
        if(access_token_map==null){
            return null;
        }
        //获取用户信息
        String openid = access_token_map.get("openid");
        String access_token = access_token_map.get("access_token");
        //拿access_token查询用户信息
        Map<String, String> userinfo = getUserinfo(access_token, openid);
        if(userinfo==null){
            return null;
        }

        //添加用户到数据库
        XcUser xcUser = null;

        return xcUser;
    }

测试获取用户信息

1、在获取用户信息处打断点

2、进入http://www.51xuecheng.cn/wxsign.html

3、手机扫码授权

保存用户信息

向数据库保存用户信息,如果用户不存在将其保存在数据库。

@Autowired
XcUserRoleMapper xcUserRoleMapper;

@Transactional
public XcUser addWxUser(Map userInfo_map){
    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");//学生角色
    xcUserRoleMapper.insert(xcUserRole);
    return xcUser;
}

 调用保存用户信息

@Autowired
WxAuthServiceImpl currentProxy;

public XcUser wxAuth(String code){

    //收到code调用微信接口申请access_token
    Map<String, String> access_token_map = getAccess_token(code);
    if(access_token_map==null){
        return null;
    }
    System.out.println(access_token_map);
    String openid = access_token_map.get("openid");
    String access_token = access_token_map.get("access_token");
    //拿access_token查询用户信息
    Map<String, String> userinfo = getUserinfo(access_token, openid);
    if(userinfo==null){
        return null;
    }
    //将用户信息保存到数据库
    XcUser xcUser = currentProxy.addWxUser(userinfo);
    return xcUser;
}

 

测试保存用户信息

1、在保存用户信息处打断点

2、进入http://www.51xuecheng.cn/wxsign.html

3、手机扫码授权

4、自动跳转到登录页面,提交认证成功。

  • 14
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

拉普兰德做的到吗?

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值