目录
1、用户认证
1. 需求分析
至此我们了解了使用Spring Security进行认证授权的过程,本节实现用户认证功能。
目前各大网站的认证方式非常丰富:账号密码认证、手机验证码认证、扫码登录等。
这里有不懂的可以去看:OAuth2认证流程_Relievedz的博客-CSDN博客
2、连接用户中心数据库
1. 连接数据库认证
基于的认证流程在研究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 类型的用户信息即可。
首先屏蔽原来定义的UserDetailsService。 :这是自己写的代码
//配置用户信息服务 // @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 自己编写
package com.xuecheng.ucenter.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; import com.xuecheng.ucenter.mapper.XcUserMapper; import com.xuecheng.ucenter.model.po.XcUser; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; 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.Component; /** * @program: xuecheng-plus-project148 * @description: 接口查询数据库得到用户信息返回UserDetails 类型的用户信息即可。 * @author: Mr.Zhang * @create: 2023-03-17 08:52 **/ @Slf4j @Component public class UserServiceImpl implements UserDetailsService { @Autowired XcUserMapper userMapper; /** * 根据账号查询用户信息 * @param s 账号 * @return * @throws UsernameNotFoundException */ @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { //账号 String username = s; //根据username账号查询数据库 XcUser xcUser = userMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, username)); //查询到用户不存在,要返回null即可,spring security框架抛出异常用户不存在 if (xcUser==null){ return null; } //如果查到了用户拿到正确的密码,最终封装成一个UserDetails对象给spring security框架返回,由框架进行密码对比 String password = xcUser.getPassword(); // //用户权限,如果不加报Cannot pass a null GrantedAuthority collection String[] authorities= {"test"}; //创建UserDetails对象,权限信息待实现授权功能时再向UserDetail中加入 UserDetails userDetails = User.withUsername(username).password(password).authorities(authorities).build(); return userDetails; } }
写到这里我们需要清楚框架调用loadUserByUsername()方法拿到用户信息之后是如何执行的,见下图:
数据库中的密码加过密的,用户输入的密码是明文,我们需要修改密码格式器PasswordEncoder,原来使用的是NoOpPasswordEncoder,它是通过明文方式比较密码,现在我们修改为BCryptPasswordEncoder,它是将用户输入的密码编码为BCrypt格式与数据库中的密码进行比对。
如下:
@Bean public PasswordEncoder passwordEncoder() { // //密码为明文方式 // return NoOpPasswordEncoder.getInstance(); return new BCryptPasswordEncoder(); }
我们通过测试代码测试BCryptPasswordEncoder,如下:
//进行密码比对public static void main(String[] args) { String password = "111111"; BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); for (int i = 0; i < 5; i++) { //生成密码 String encode = passwordEncoder.encode(password); System.out.println(encode); //校验密码,参数1是输入的明文,参数2是正确密码加密后的串 boolean matches = passwordEncoder.matches(password, encode); System.out.println(matches); } boolean matches = passwordEncoder.matches("1111", "$2a$10$Q0ItVMXc/VwrlRE7NGBFtetut6o7vxpSjQDcKvtakIrCnxOWf.LV."); System.out.println(matches); }
修改数据库中的密码为Bcrypt格式,并且记录明文密码,稍后申请令牌时需要。
由于修改密码编码方式还需要将客户端的密钥更改为Bcrypt格式.
//客户端详情服务 @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.xuecheng-plus.com") ; }
现在重启认证服务。
下边使用httpclient进行测试:
### 密码模式 POST {{auth_host}}/auth/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username=stu2&password=111111
参数介绍:
### 密码模式 POST {{服务器的端口号}}/auth/oauth/token? 路径client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username=t1(我数据库的用户名)&password=111111(数据库的密码)
输入正确的账号和密码,申请令牌成功。
输入错误的密码,报错:
{
"error": "invalid_grant",
"error_description": "用户名或密码错误"
}
输入错误的账号,报错:
{
"error": "unauthorized",
"error_description": "UserDetailsService returned null, which is an interface contract violation"
}
输入正确:
2、扩展用户身份信息
用户表中存储了用户的账号、手机号、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如下:
package com.xuecheng.ucenter.service.impl; import com.alibaba.fastjson.JSON; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; import com.xuecheng.ucenter.mapper.XcUserMapper; import com.xuecheng.ucenter.model.po.XcUser; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; 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.Component; /** * @program: xuecheng-plus-project148 * @description: 接口查询数据库得到用户信息返回UserDetails 类型的用户信息即可。 * @author: Mr.Zhang * @create: 2023-03-17 08:52 **/ @Slf4j @Component public class UserServiceImpl implements UserDetailsService { @Autowired XcUserMapper userMapper; /** * 根据账号查询用户信息 * @param s 账号 * @return * @throws UsernameNotFoundException */ @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { //账号 String username = s; //根据username账号查询数据库 XcUser xcUser = userMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, username)); //查询到用户不存在,要返回null即可,spring security框架抛出异常用户不存在 if (xcUser==null){ return null; } //如果查到了用户拿到正确的密码,最终封装成一个UserDetails对象给spring security框架返回,由框架进行密码对比 String password = xcUser.getPassword(); // //用户权限,如果不加报Cannot pass a null GrantedAuthority collection String[] authorities= {"test"}; xcUser.setPassword(null); //创建UserDetails对象,权限信息待实现授权功能时再向UserDetail中加入 //将用户信息转json String userJson = JSON.toJSONString(xcUser); UserDetails userDetails = User.withUsername(userJson).password(password).authorities(authorities).build(); return userDetails; } }
重启认证服务,重新生成令牌,生成成功。
我们可以使用check_token查询jwt的内容
###校验jwt令牌 POST {{auth_host}}/auth/oauth/check_token?token=
响应示例如下,
{
"aud": [
"xuecheng-plus"
],
"user_name": "{\"createTime\":\"2022-09-28T08:32:03\",\"id\":\"51\",\"name\":\"学生2\",\"sex\":\"1\",\"status\":\"1\",\"username\":\"stu2\",\"utype\":\"101001\"}",
"scope": [
"all"
],
"active": true,
"exp": 1679025018,
"authorities": [
"test"
],
"jti": "14c4085b-7787-4ce8-8c24-90f1fc80df34",
"client_id": "XcWebApp"
}
user_name存储了用户信息的json格式,在资源服务中就可以取出该json格式的内容转为用户对象去使用。
3、资源服务获取用户身份
下边编写一个工具类在各个微服务中去使用,获取当前登录用户的对象。
package com.xuecheng.content.util; import com.alibaba.fastjson.JSON; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.context.SecurityContextHolder; import java.io.Serializable; import java.time.LocalDateTime; /** * @program: xuecheng-plus-project148 * @description: 获取当前用户身份工具类 * @author: Mr.Zhang * @create: 2023-03-17 10:05 **/ @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 Long courseId){ //获取当前用户的身份 // Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); // System.out.println("身份验证"+principal); SecurityUtil.XcUser user = SecurityUtil.getUser(); System.out.println(user.getUsername()); CourseBaseInfoDto courseBaseInfo = courseBaseInfoService.getCourseBaseInfo(courseId); return courseBaseInfo; }
重启内容管理服务:
1、启动认证服务、网关、内容管理服务
2、生成新的令牌
3、携带令牌访问内容管理服务的查询课程接口