😎 知识点概览
为了方便后续回顾该项目时能够清晰的知道本章节讲了哪些内容,并且能够从该章节的笔记中得到一些帮助,所以在完成本章节的学习后在此对本章节所涉及到的知识点进行总结概述。
本章节为【学成在线】项目的 day17
的内容
- 构建用户中心服务,并基于
Spring Security Oauth2
以及jwt
令牌实现用户认证的完整流程。 - 完成门户网站的用户登入、登出接口、前端页面的开发以及调试。
- 基于 Zuul 构建网关服务,以及使用 Zuul 网关实现基本的路由转发、过滤器、身份校验等功能。
目录
内容会比较多,小伙伴们可以根据目录进行按需查阅。
一、用户认证
0x01 用户认证流程分析
用户认证流程如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QeiDrDHT-1595567384585)(https://qnoss.codeyee.com/20200704_MTc=/image1.png)]
业务流程说明如下:
1、客户端请求认证服务进行认证。
2、认证服务认证通过向浏览器 cookie
写入 token
(身份令牌)
认证服务请求用户中心查询用户信息。
认证服务请求 Spring Security
申请令牌。
认证服务将 token
(身份令牌)和 jwt
令牌存储至 redis
中。
认证服务向cookie写入 token
(身份令牌)。
3**、前端携带token请求认证服务获取**jwt令牌
前端获取到 jwt
令牌并存储在 sessionStorage
。
前端从jwt令牌中解析中用户信息并显示在页面。
前端如何解析?还是认证服务返回明文数据
4**、前端携带cookie中的token身份令牌及jwt令牌访问资源服务**
前端请求资源服务需要携带两个token,一个是cookie中的身份令牌,一个是http header中的jwt令牌
前端请求资源服务前在http header上添加jwt请求资源
5、网关校验 token的合法性
用户请求必须携带 token
身份令牌和jwt令牌
网关校验redis中 token
是否合法,已过期则要求用户重新登录
6、资源服务校验jwt的合法性并完成授权
资源服务校验jwt令牌,完成授权,拥有权限的方法正常执行,没有权限的方法将拒绝访问。
0x02 认证服务查询数据库
需求分析
-
认证服务根据数据库中的用户信息去校验用户的身份,即校验账号和密码是否匹配。
-
认证服务不直接连接数据库,而是通过用户中心服务去查询用户中心数据库。
完整的流程图如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fQ7m6VrN-1595567384594)(https://qnoss.codeyee.com/20200704_MTc=/image3.png)]
搭建环境
1、创建用户中心数据库
用户中心负责用户管理,包括:用户信息管理、角色管理、权限管理等。
创建 xc_user
数据库(MySQL)
导入 xc_user.sql
(已导入不用重复导入)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-017D2qLJ-1595567384600)(https://qnoss.codeyee.com/20200704_MTc=/image4.png)]
2、创建用户中心工程
导入“资料”-》xc-service-ucenter.zip
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F5bvUFkv-1595567384604)(https://qnoss.codeyee.com/20200704_MTc=/image5.png)]
完成用户中心根据账号查询用户信息接口功能。
查询用户接口开发
1、Api接口
用户中心对外提供如下接口
1)响应数据类型
此接口将来被用来查询用户信息及用户权限信息,所以这里定义扩展类型
package com.xuecheng.framework.domain.ucenter.response.ext;
import com.xuecheng.framework.domain.ucenter.XcMenu;
import com.xuecheng.framework.domain.ucenter.XcUser;
import lombok.Data;
import lombok.ToString;
import java.util.List;
@Data
@ToString
public class XcUserExt extends XcUser {
//权限信息
private List<XcMenu> permissions;
//企业信息
private String companyId;
}
2)根据账号查询用户信息
package com.xuecheng.api.ucenter;
import com.xuecheng.framework.domain.ucenter.response.ext.XcUserExt;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
@Api(value = "用户中心",description = "用户中心管理")
public interface UcenterControllerApi {
@ApiOperation("获取用户信息")
public XcUserExt getUserext(String username);
}
2、DAO
添加 XcUser
、XcCompantUser
两个表的Dao ,对于一些简单的sql操作,我们使用 Spring Data JPA 实现
public interface XcUserRepository extends JpaRepository<XcUser, String> {
XcUser findXcUserByUsername(String username);
}
public interface XcCompanyUserRepository extends JpaRepository<XcCompanyUser,String> {
//根据用户id查询所属企业id
XcCompanyUser findByUserId(String userId);
}
3、Service
@Service
public class UserServiceImpl implements UserService {
//对xc_user表的相关操作
@Autowired
XcUserRepository xcUserRepository;
//对xc_company_user表的相关操作
@Autowired
XcCompanyUserRepository xcCompanyUserRepository;
/**
* 根据用户名查询用户信息的实现
* @param username
* @return
*/
@Override
public XcUser findXcUserByUsername(String username) {
return xcUserRepository.findXcUserByUsername(username);
}
/**
* 根据用户名获取用户权限的实现
* @param username 用户名
* @return
*/
@Override
public XcUserExt getUserExt(String username) {
//查询用户信息
XcUser xcUser = this.findXcUserByUsername(username);
if(xcUser ==null) return null;
XcUserExt xcUserExt = new XcUserExt();
BeanUtils.copyProperties(xcUser,xcUserExt);
//根据用户id查询用所属公司
String xcUserId = xcUser.getId();
XcCompanyUser xcCompanyUser = xcCompanyUserRepository.findByUserId(xcUserId);
if(xcCompanyUser!=null){
String companyId = xcCompanyUser.getCompanyId();
xcUserExt.setCompanyId(companyId);
}
//返回XcUserExt对象
return xcUserExt;
}
}
4、Controller
@RestController
@RequestMapping("/ucenter")
public class UcenterController implements UcenterControllerApi {
@Autowired
UserService userService;
@Override
@GetMapping("/getuserext")
public XcUserExt getUserext(@RequestParam("username") String username) {
XcUserExt xcUser = userService.getUserExt(username);
return xcUser;
}
}
5、可能出现的一些问题
如果 ucenter
服务出现接口需要认证才能访问的情况,考虑可能是继承了 model
工程的 oauth2
依赖导致开启了认证拦截。
解决方案:在 model 工程下的 oauth2
依赖加上 <optional>true</optional>
标签,该标签可以防止本工程下的依赖包传递到其他工程。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<optional>true</optional>
</dependency>
6、测试
使用 Swagger-ui
或 postman
测试用户信息查询接口
GET http://localhost:40300/ucenter/getuserext
参数为 username
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IFnI31Zd-1595567384606)(https://qnoss.codeyee.com/20200704_MTc=/image6.png)]
7、思考一些问题
在上述测试过程中,通过 GET 请求调用 http://localhost:40300/ucenter/getuserext 接口可以获取到一个用户的详细信息,但是考虑到用户数据的安全问题,这个接口不应该直接暴露给普通的用户,只适合服务间的调用,并需要经过授权的服务才可以调用。
答:后期配置微服务间认证后可以解决上述的问题。
调用查询用户的接口
1、创建 client
认证服务需要远程调用用户中心服务查询用户,在 认证服务
中创建Feign客户端
@FeignClient(value = XcServiceList.XC_SERVICE_UCENTER)
public interface UserClient {
@GetMapping("/ucenter/getuserext")
public XcUserExt getUserext(@RequestParam("username") String username)
}
2、UserDetailsService
认证服务调用 spring security
接口申请令牌,spring security
接口会调用 UserDetailsServiceImpl
从数据库查询用户,如果查询不到则返回 NULL
,表示不存在;在UserDetailsServiceImpl
中将正确的密码返回, spring security
会自动去比对输入密码的正确性。
修改 UserDetailsServiceImpl 的 loadUserByUsername
方法,调用 Ucenter服务的查询用户接口
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
ClientDetailsService clientDetailsService;
//用户中心服务客户端
@Autowired
UserClient userClient;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//取出身份,如果身份为空说明没有认证
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//没有认证统一采用httpbasic认证,httpbasic中存储了client_id和client_secret
//开始认证client_id和client_secret
if(authentication==null){
ClientDetails clientDetails = clientDetailsService.loadClientByClientId(username);
if(clientDetails!=null){
//密码
String clientSecret = clientDetails.getClientSecret();
return new User(username,clientSecret,AuthorityUtils.commaSeparatedStringToAuthorityList(""));
}
}
if (StringUtils.isEmpty(username)) {
return null;
}
//请求ucenter查询用户
XcUserExt userext = userClient.getUserext(username);
if(userext == null) return null; //如果获取到的用信息为空,则返回null,spring security则会抛出异常
//设置用户的认证和权限信息
userext.setUsername("itcast");
userext.setPassword(new BCryptPasswordEncoder().encode("123"));
userext.setPermissions(new ArrayList<XcMenu>()); //这里授权部分还没完成,所以先填写静态的
if(userext == null){
return null;
}
//从数据库查询用户正确的密码,Spring Security会去比对输入密码的正确性
String password = userext.getPassword();
String user_permission_string = "";
//设置用户信息到userDetails对象
UserJwt userDetails = new UserJwt(
username,
password,
AuthorityUtils.commaSeparatedStringToAuthorityList(user_permission_string));
//用户id
userDetails.setId(userext.getId());
//用户名称
userDetails.setName(userext.getName());
//用户头像
userDetails.setUserpic(userext.getUserpic());
//用户所属企业id
userDetails.setCompanyId(userext.getCompanyId());
//返回用信息给到Spring Security进行处理
return userDetails;
}
}
3、BCryptPaswordEncoder
早期使用md5对密码进行编码,每次算出的md5值都一样,这样非常不安全,Spring Security推荐使用
BCryptPasswordEncoder对密码加随机盐,每次的Hash值都不一样,安全性高 。
1)BCryptPasswordEncoder测试程序如下
@Test
public void testPasswrodEncoder(){
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);
}
}
2)在 AuthorizationServerConfig
配置类中配置 BCryptPasswordEncoder
原教程中已经在 WebSecurityConfig 中进行了配置,这个在哪里配置都无所谓,本质上都是向spring注入一个bean
//采用bcrypt对密码进行Hash
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
3)测试
请求 http://localhost:40400/auth/userlogin,输入正常的账号和密码进行测试
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tui8jCcF-1595567384607)(https://qnoss.codeyee.com/20200704_MTc=/image7.