04-获取用户身份信息认证

用户认证

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

查询用户信息

认证服务所需要的用户信息存储在xc_user数据库中,之前我们是将用户信息硬编码放在内存中的,现在我们要从数据库中查询用户信息

  • InMemoryUserDetailsManagerUserDetailsService接口的实现类,可以在内存中配置用户的信息
package com.xuecheng.auth.config;
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public UserDetailsService userDetailsService() {
        // 1. 配置用户信息服务
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        // 2. 创建用户信息这里暂时写死,后面需要从数据库中动态查询, Kyle的权限是p1,Lucy的权限是p2
        // User是Spring Security提供的工具类
        manager.createUser(User.withUsername("Kyle").password("123").authorities("p1").build());
        manager.createUser(User.withUsername("Lucy").password("456").authorities("p2").build());
        return manager;
    }
    // 设置密码比对的规则
    @Bean
    public PasswordEncoder passwordEncoder() {
        // 采用明文的方式直接比对
        return NoOpPasswordEncoder.getInstance();
    }

    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/r/**")// 表示"/r/**"开头的请求需要认证
            .authenticated()
            .anyRequest().permitAll()
            .and()
            .formLogin()
            .successForwardUrl("/login-success");
        // 配置退出登录页面,认证成功后访问/logout可退出登录
        http.logout().logoutUrl("/logout");
    }
}

用户提交账号和密码后由DaoAuthenticationProvider调用UserDetailsServiceloadUserByUsername()方法获取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();
}

04-获取用户身份信息认证_第1张图片

屏蔽掉原先的InMemoryUserDetailsManager, 自定义一个UserDetailsService接口的实现类UserDetailsImpl,重写loadUserByUsername()方法

  • 我们重写方法的业务逻辑是根据用户的账号名从数据库中查询对应的用户信息,然后将查询到的用户名和密码封装到UserDetails类型的对象中
  • 返回给Spring Security框架的UserDetails对象不仅要设置用户名和密码还要设置用户权限,如果不加报Cannot pass a null GrantedAuthority collection
  • 我们只需要给把UserDetails(含密码)对象返回给Spring Security框架返回,框架会把用户的登陆密码和数据库中查询到的密码进行比对
@Service
public class UserDetailsImpl implements UserDetailsService {
    @Autowired
    XcUserMapper xcUserMapper;
    /**
    *
    * @param name     用户输入的登录账号
    * @return         UserDetails
    * @throws         UsernameNotFoundException
    */
    @Override
    public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
        // 根据username去XcUser表中查询对应的用户信息
        XcUser user = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, name));
        // 返回NULL表示用户不存在,SpringSecurity会帮我们处理即抛出异常表示用户不存在
        if (user == null) {
            return null;
        }
        // 设置用户权限
        // String[] authorities=  {"test"};
        
        // 取出数据库中用户的密码
        String password = user.getPassword();
        // 将用户名和密码以及用户权限(权限必须要设置)封装成一个UserDetails对象,然后返回即可
        return User.withUsername(user.getUsername()).password(password).authorities("test").build();
    }
}

校验密码

数据库中的密码是加密过的,但用户登陆时输入的密码是明文形式,所以我们要修改WebSecurityConfig配置类中的密码格式器PasswordEncoder类型

  • NoOpPasswordEncoder: 表示通过明文的方式比较密码
  • BCryptPasswordEncoder: 表示先将用户输入的密码编码为BCrypt格式后才与数据库中的密码进行比对
@Bean
public PasswordEncoder passwordEncoder() {     
	// return NoOpPasswordEncoder.getInstance();
    return new BCryptPasswordEncoder();
}

BCryptPasswordEncoder原理: 虽然同样的原始密码可以对应不同的Hash值,但是比对密码时如果调用它提供的matches方法即使Hash值不同但校验是正确的

  • 先将数据库中的密码存储的方式改为BCrype格式, 记录其对应的明文密码申请令牌时需要
public static void main(String[] args) {
    String password = "123456";
    BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
    for (int i = 0; i < 5; i++) {
        // 每个计算出的Hash值都不一样
        String encodePsw = encoder.encode(password);
        // 虽然Hash值不一样,但是校验是可以通过的
        System.out.println("转换后密码:" + encodePsw + "比对情况:" + encoder.matches(password, encodePsw));
    }
}

// 转换后密码:$2a$10$6hbvtCtgcISvbBHJ.UnhPO1io7StF.ySPkmAvzO/efvNmHVVJZOeK比对情况:true
// 转换后密码:$2a$10$ufYW9qXSAk0N201B/wCR7uGrzygawnwXtyL2vKpDLAOCOkF33sGnK比对情况:true
// 转换后密码:$2a$10$DEaVxYHakIE/kDvAU4eC7OZ7c9kqKBJedClVxDPnYH.zwuZvCRnzm比对情况:true
// 转换后密码:$2a$10$s2qgaKGgULYQ7tce2u6TIeHopap4HqfyghJYu1vdDZ2WcNk70ykFe比对情况:true
// 转换后密码:$2a$10$XQaQJIfXyd/UvMHC..uBNuDXNVrZHnEGn.tW0oSB6WVjdsZLFpkGq比对情况:true

由于我们修改了密码的编码方式,所以还还需要将认证服务批准接入的客户端的密钥更改为更改为BCrypt格式

// 配置客户端的详情信息
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    clients.inMemory()// 使用in-memory存储
            .withClient("XcWebApp")// 客户端的Id
            .secret(new BCryptPasswordEncoder().encode("XcWebApp"))//客户端密钥,基于BCrypt格式
            .resourceIds("xuecheng-plus")//资源列表
            .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")// 允许的授权类型
            .scopes("all")// 允许的授权范围
            .autoApprove(false)//false跳转到授权页面                  
            .redirectUris("http://localhost/");//客户端接收授权码的重定向地址
}

重启认证服务使用HttpClient通过密码模式申请令牌,保证登陆的明文密码在数据库中有对应的BCrypt格式的加密密码

###### 密码模式
POST localhost:53070/auth/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username=Kyle&password=111111

查看服务端响应的结果

// 输入正确的账号密码即可成功申请令牌
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsieHVlY2hlbmctcGx1cyJdLCJ1c2VyX25hbWUiOiJLeWxlIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTY3ODQ1MTUxNiwiYXV0aG9yaXRpZXMiOlsidGVzdCJdLCJqdGkiOiJkOWUwYjU0ZS03Zjg4LTQ2NjAtYjFlZS04ZWQzYjYzZmQwNjMiLCJjbGllbnRfaWQiOiJYY1dlYkFwcCJ9.NaS3hmpDtX3zkXvnZWoo9ROEWgYeA6GoxBzy_lOxzvA",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsieHVlY2hlbmctcGx1cyJdLCJ1c2VyX25hbWUiOiJLeWxlIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6ImQ5ZTBiNTRlLTdmODgtNDY2MC1iMWVlLThlZDNiNjNmZDA2MyIsImV4cCI6MTY3ODcwMzUxNiwiYXV0aG9yaXRpZXMiOlsidGVzdCJdLCJqdGkiOiI5YjE4OTUwOS1iYmEzLTRkMTctYTNkNC05OWQwZGI5NjU0MDAiLCJjbGllbnRfaWQiOiJYY1dlYkFwcCJ9.548w5CdQiIU5_k1qRBjzM-PMBqy-XX3zr17tQS6g6CM",
"expires_in": 7199,
"scope": "all",
"jti": "d9e0b54e-7f88-4660-b1ee-8ed3b63fd063"
}

// 输入错误的密码,申请令牌失败
{
"error": "invalid_grant",
"error_description": "用户名或密码错误"
}

扩展用户身份信息

用户表中除了存储用户的账号密码还有其他信息,但是UserDetails接口中只有username、password字段,这样JWT令牌中也只会写入用户的账号和密码

  • 在认证阶段DaoAuthenticationProvider会调用UserDetailsService查询数据库表中用户全部的信息,但是UserDetails中只能存储账号和密码
public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();
    String getPassword();
    String getUsername();
    boolean isAccountNonExpired();
    boolean isAccountNonLocked();
    boolean isCredentialsNonExpired();
    boolean isEnabled();
}

由于JWT令牌中用户身份信息来源于UserDetails, 所以我们只能把UserDetails中包含的用户信息写入JWT令牌中,想要扩展用户的身份信息有两种方式

  • 修改源码: 对框架提供的UserDetails进行扩展,使之包括更多的自定义属性
  • 存储Json(推荐): 由于UserDetails的username属性不仅可以只存储用户账号,我们还可以将用户的全部信息转为Json数据作为username的内容
@Service
public class UserDetailsImpl implements UserDetailsService {
    @Autowired
    XcUserMapper xcUserMapper;
    /**
    *
    * @param name     用户输入的登录账号
    * @return         UserDetails
    * @throws         UsernameNotFoundException
    */
    @Override
    public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
        // 根据username去XcUser表中查询对应的用户信息
        XcUser user = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, name));
        // 返回NULL表示用户不存在,SpringSecurity会帮我们处理即抛出异常表示用户不存在
        if (user == null) {
            return null;
        }
        // 设置用户权限
        // String[] authorities=  {"test"};
        // 取出数据库中用户的密码
        String password = user.getPassword();
        
        // 将查询到的用户信息映射的XcUser对象转为Json,注意对用户的敏感信息要进行脱敏
        user.setPassword(null);
        String userString = JSON.toJSONString(user);
        // 将用户名和密码以及用户权限(权限必须要设置)封装成一个UserDetails对象,然后返回即可
        return User.withUsername(userString).password(password).authorities("test").build();
    }
}

重启认证服务,使用密码模式重新获取令牌

// 密码模式
POST localhost:53070/auth/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username=Kyle&password=111111

访问校验接口查看JWT令牌中的内容,此时令牌的user_name属性中包含了查询到的用户全部信息(密码为null)

// 访问校验接口
POST localhost:53070/auth/oauth/check_token?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsieHVlY2hlbmctcGx1cyJdLCJ1c2VyX25hbWUiOiJ7XCJjb21wYW55SWRcIjpcIjEyMzIxNDE0MjVcIixcImNyZWF0ZVRpbWVcIjpcIjIwMjItMDktMjhUMDg6MzI6MDNcIixcImlkXCI6XCI1MlwiLFwibmFtZVwiOlwiS2lraVwiLFwicGFzc3dvcmRcIjpcIiQyYSQxMCQwcHQ3V2xmVGJuUERUY1d0cC8uMk11NUNUWHZvaG5OUWhSNjI4cXE0Um9LU2MwZEdBZEVnbVwiLFwic2V4XCI6XCIxXCIsXCJzdGF0dXNcIjpcIlwiLFwidXNlcm5hbWVcIjpcIkt5bGVcIixcInV0eXBlXCI6XCIxMDEwMDJcIn0iLCJzY29wZSI6WyJhbGwiXSwiZXhwIjoxNjc4NDUyMzU0LCJhdXRob3JpdGllcyI6WyJ0ZXN0Il0sImp0aSI6Ijc2MDc0MDI4LTBiM2MtNDQ4Mi1hN2Y0LTc1NDI3ZTA2OTFjMSIsImNsaWVudF9pZCI6IlhjV2ViQXBwIn0._GKfGE2s5k0n6VC4_RKQrzdzydWY-WtX3Q_Hc4DxQ1g

{
  "aud": [
    "xuecheng-plus"
  ],
  "user_name": "{\"companyId\":\"1232141425\",\"createTime\":\"2022-09-28T08:32:03\",\"id\":\"52\",\"name\":\"Kiki\",\"password\":\"$2a$10$0pt7WlfTbnPDTcWtp/.2Mu5CTXvohnNQhR628qq4RoKSc0dGAdEgm\",\"sex\":\"1\",\"status\":\"\",\"username\":\"Kyle\",\"utype\":\"101002\"}",
  "scope": [
    "all"
  ],
  "active": true,
  "exp": 1678452354,
  "authorities": [
    "test"
  ],
  "jti": "76074028-0b3c-4482-a7f4-75427e0691c1",
  "client_id": "XcWebApp"
}

资源服务获取用户身份信息

现在认证服务生成JWT令牌的user_name属性已经存储用户JSON格式的信息,在资源服务中就可以取出该JSON格式的内容转换为用户对象去使用

第一步: 在content-api工程中写一个工具类SecurityUtil用来在各个微服务中获取到当前登录用户信息的对象

@Slf4j
public class SecurityUtil {
    public static XcUser getUser() {
        try {
            // 通过SecurityContextHolder获取user_name属性的值即包含用户信息的Json字符串
            Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
            if (principal instanceof String) {
                // 将包含用户信息的Json字符串转换为XcUser对象
                String userJson = principal.toString();
                XcUser xcUser = JSON.parseObject(userJson, XcUser.class);
                return xcUser;
            }
        } catch (Exception e) {
            log.error("获取当前登录用户身份信息出错:{}", e.getMessage());
            e.printStackTrace();
        }
        return null;
    }
    // 这里使用内部类是为了不让content工程去依赖auth工程
    @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 Long courseId) {
    SecurityUtil.XcUser user = SecurityUtil.getUser();
    System.out.println("当前用户身份为:" + user);
    return courseBaseInfoService.getCourseBaseInfo(courseId);
}

第三步: 启动认证服务、网关、内容管理服务, 访问认证服务生成令牌,携带生成的令牌访问内容管理服务的查询课程接口,在控制台中查看登陆的用户信息

当前用户身份为:SecurityUtil.XcUser(id=52, username=Kyle, password=null, salt=null, name=Kiki, nickname=null, wxUnionid=null, companyId=1232141425, userpic=null, utype=101002, birthday=null, sex=1, email=null, cellphone=null, qq=null, status=, createTime=2022-09-28T08:32:03, updateTime=null)

认证方式多样化

统一认证入口

目前各大网站支持账号密码认证、手机验证码认证、扫码登录认证等多种认证方式,Spring Security框架也支持多样化的认证方案

  • 账号和密码认证: 采用OAuth2协议的密码模式即可实现
  • 手机号加验证码认证: 用户认证提交的是手机号和验证码并不是账号和密码
  • 微信扫码认证: 基于OAuth2协议,目标网站会向微信服务器申请一个令牌,然后携带令牌去微信服务端查询用户信息,查询成功则用户在目标网站认证通过

第一步: 由于不同的认证提交方式的数据不一样如手机加验证码方式和账号加密码加验证码方式,所以我们需要创建一个DTO类用于接收各种认证参数

@Data
public class AuthParamsDto {
    // 用户名
    private String username; 
	// 域,用于扩展	
    private String password; 
	// 手机号
    private String cellphone;
	// 验证码
    private String checkcode;
	// 验证码对应的存储在Redis中的key
    private String checkcodekey;
	// 认证的类型,如password表示用户名密码模式类型,sms表示短信模式类型
    private String authType; 
	// 附加数据,不同认证类型可拥有不同的附加数据,如认证类型为短信时包含smsKey,另外所有认证都会包含clientId
    private Map<String, Object> payload = new HashMap<>();
}

第二步: 修改loadUserByUsername()方法,将用户提交的请求参数username的值改为JSON字符串,用来包含不同的认证方式所提交的各种参数

  • 由DaoAuthenticationProvider调用我们自定义的UserDetailsService接口实现类UserDetailsImplloadUserByUsername()方法获取UserDetails用户信息
@Service
public class UserDetailsImpl implements UserDetailsService {
    @Autowired
    XcUserMapper xcUserMapper;
    /**
     * @param s AuthParamsDto类型的json数据
     * @return UserDetails
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
   		// 将包含认证参数的Json格式的字符串如`username={"username":"Kyle","authType":"password","password":"111111"}`转换为AuthParamsDto对象
        AuthParamsDto authParamsDto = null;
        try {
            authParamsDto = JSON.parseObject(s, AuthParamsDto.class);
        } catch (Exception e) {
            log.error("认证请求数据格式不对:{}", s);
            throw new RuntimeException("认证请求数据格式不对");
        }
        // 根据username去XcUser表中查询对应的用户信息
        String name = authParamsDto.getUsername();
        XcUser user = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, name));
        // 返回空表示用户不存在,SpringSecurity会帮我们处理
        if (user == null) {
            return null;
        }
        // 取出数据库存储的密码
        String password = user.getPassword();
        user.setPassword(null);
        String userString = JSON.toJSONString(user);
        // 创建UserDetails对象并返回,注意这里的authorities必须指定
        return User.withUsername(userString).password(password).authorities("test").build();
    }
}

DaoAuthenticationProvider获取到UserDetails用户信息后会调用自身的additionalAuthenticationChecks()方法进行密码校验

  • DaoAuthenticationProviderCustom类实现了DaoAuthenticationProvider接口并实现了additionalAuthenticationChecks()方法
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    if (authentication.getCredentials() == null) {
        this.logger.debug("Authentication failed: no credentials provided");
        throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
    } else {
        String presentedPassword = authentication.getCredentials().toString();
        if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
            this.logger.debug("Authentication failed: password does not match stored value");
            throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }
    }
} 	

但是并不是所有的校验方式都需要密码,所以我们要自定义密码的校验逻辑

  • 自定义DaoAuthenticationProviderCustom类继续DaoAuthenticationProviderCustom类然后重写additionalAuthenticationChecks方法
@Component
public class DaoAuthenticationProviderCustom extends DaoAuthenticationProvider {
    // 由于DaoAuthenticationProvider调用UserDetailsService,所以这里需要注入一个UserDetailsService接口的实现类
    @Autowired
    public void setUserDetailsService(UserDetailsService userDetailsService){
        super.setUserDetailsService(userDetailsService);
    }
    // 屏蔽密码对比,因为不是所有的认证方式都需要校验密码
    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        // 里面啥也不写就不会校验密码了
    }
}

修改认证服务工程下的config/WebSecurityConfig类,使用我们自定义的DaoAuthenticationProviderCustom

package com.xuecheng.auth.config;
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    DaoAuthenticationProviderCustom daoAuthenticationProviderCustom;

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

重启认证服务,测试申请令牌接口将传入的账号信息改为包含各种认证参数的JSON格式字符串

#### 密码模式
POST localhost:53070/auth/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username={"username":"Kyle","authType":"password","password":"111111"}

自定义认证方式

有了认证参数对象AuthParamsDto,我们可以定义一个Service接口去进行各种方式的认证,通过Service接口的各种实现类来实现各种方式的认证

04-获取用户身份信息认证_第2张图片

第一步: 定义用户信息,因为XcUser类对应的是数据库中的表后期不便修改,所以为了可扩展性定义一个XcUserExt类让其继承XcUser

@Data
public class XcUserExt extends XcUser {

}

第二步: 定义认证的AuthService接口

/**
 * 认证Service
 */
public interface AuthService {
    /**
     * 认证方法
     * @param authParamsDto 认证参数
     * @return  用户信息
     */
    XcUserExt execute(AuthParamsDto authParamsDto);
}

第三步: 定义AuthService接口的实现类PasswordAuthServiceImpl(实现密码认证)``WxAuthServiceImpl(扫码认证)和,依靠beanName区分不同的认证方式

@Service("password_authservice")
public class PasswordAuthServiceImpl implements AuthService {
    @Override
    public XcUserExt execute(AuthParamsDto authParamsDto) {
        return null;
    }
}
@Service("wx_authservice")// 微信扫码方式
public class WxAuthServiceImpl implements AuthService {

    @Override
    public XcUserExt execute(AuthParamsDto authParamsDto) {
        return null;
    }
}

第四步: 修改loadUserByUsername()方法,增加判断认证类型的逻辑

  • 虽然不同的认证方式的认证逻辑不同,但最后都是调用AuthService接口实现类的execute方法,查询数据库中的用户信息并返回扩展的对象XcUserExt
  • 由于所有的认证方式都会将查询得到的XcUserExt对象的信息封装到UserDetails对象中,所以我们可以将这一步抽取成一个方法
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
    // 将包含认证参数的Json格式的字符串如`username={"username":"Kyle","authType":"password","password":"111111"}`转换为AuthParamsDto对象
    AuthParamsDto authParamsDto = null;
    try {
        authParamsDto = JSON.parseObject(s, AuthParamsDto.class);
    } catch (Exception e) {
        log.error("认证请求数据格式不对:{}", s);
        throw new RuntimeException("认证请求数据格式不对");
    }
    // 获取认证类型,beanName由认证类型和后缀组成,如password+_authservice=password_authservice
    String authType = authParamsDto.getAuthType();
    // 根据认证类型从Spring容器中取出对应的bean
    AuthService authService = applicationContext.getBean(authType + "_authservice", AuthService.class);
    // 不同认证方式的认证逻辑不同,但最后都是调用统一execute方法完成认证
    XcUserExt user = authService.execute(authParamsDto);
    // 封装xcUserExt用户信息为UserDetails,根据UserDetails对象生成令牌
    return getUserPrincipal(user);
}
// 查询用户的信息
public UserDetails getUserPrincipal(XcUserExt user) {
    // 设置权限
    String[] authorities = {"test"};
    // 取出数据库中用户的密码
    String password = user.getPassword();
    user.setPassword(null);
    String userJsonStr = JSON.toJSONString(user);
    // 创建UserDetails对象保存用户脱敏后的全部信息,注意这里的authorities必须指定,如果不加报`Cannot pass a null GrantedAuthority collection`错误
    UserDetails userDetails = User.withUsername(userJsonStr).password(password).authorities(authorities).build();
    // 返回UserDetails对象
    return userDetails;
}

第五步: 重启认证服务,测试申请令牌接口注意JSON数据中要带上authType

# 密码模式,账号密码正确,可以成功获取authType,并正确查询到用户信息
POST localhost:53070/auth/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username={"username":"Kyle","password":"111111","authType":"password"}
# 测试密码错误的情况
POST localhost:53070/auth/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username={"username":"Kyle","password":"111112","authType":"password"}
# 测试账号不存在的情况
POST localhost:53070/auth/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username={"username":"Ky","password":"111112","authType":"password"}

密码认证具体实现

@Service("password_authservice")
public class PasswordAuthServiceImpl implements AuthService {
    @Autowired
    XcUserMapper xcUserMapper;
    @Autowired
    PasswordEncoder passwordEncoder;
    @Override
    public XcUserExt execute(AuthParamsDto authParamsDto) {
        // 1. 获取账号
        String username = authParamsDto.getUsername();
        // 2. 根据账号去数据库中查询用户是否存在
        XcUser xcUser = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, username));
        // 3. 不存在抛异常
        if (xcUser == null) {
            throw new RuntimeException("账号不存在");
        }
        // 4. 校验密码
        // 4.1 获取用户输入的密码
        String passwordForm = authParamsDto.getPassword();
        // 4.2 获取数据库中存储的密码
        String passwordDb = xcUser.getPassword();
        // 4.3 比较密码
        boolean matches = passwordEncoder.matches(passwordForm, passwordDb);
        // 4.4 密码不匹配抛异常
        if (!matches) {
            throw new RuntimeException("账号或密码错误");
        }
        // 4.5 密码匹配将查询得到的xcUser对象中的数据封装到扩展的xcUserExt对象中并返回
        XcUserExt xcUserExt = new XcUserExt();
        BeanUtils.copyProperties(xcUser, xcUserExt);
        return xcUserExt;
    }
}

你可能感兴趣的:(#,Spring,Security,Spring,Security,java)