用户授权业务流程
业务流程说明如下:
1、用户认证通过,认证服务向浏览器cookie写入token( 身份令牌)
2、前端携带token请求用户中心服务获取jwt令牌
前端获取到jwt令牌解析,并存储在sessionStorage
3、前端携带cookie中的身份令牌及jwt令牌访问资源服务
前端请求资源服务需要携带两个token,一个是cookie中的身份令牌,一个是http header中的jwt
前端请求资源服务前在http header上添加jwt请求资源
4、网关校验token的合法性
用户请求必须携带身份令牌和jwt令牌
网关校验redis中user_token的有效期,已过期则要求用户重新登录
5、资源服务校验jwt的合法性并进行授权
资源服务校验jwt令牌,完成授权,拥有权限的方法正常执行,没有权限的方法将拒绝访问。
一、认证服务查询数据库
认证服务根据数据库中的用户信息去校验用户的身份,即校验账号和密码是否匹配。 认证服务不直接连接数据库,而是通过用户中心服务去查询用户中心数据库。
完整的流程图如下:
创建用户中心工程xc-service-ucenter
用户中心对外提供如下接口:
根据账号查询用户信息
@Api(value = "用户中心",description = "用户中心管理")
public interface UcenterControllerApi {
public XcUserExt getUserext(String username);
}
@Data
@ToString
public class XcUserExt extends XcUser {
//权限信息
private List<XcMenu> permissions;
//企业信息
private String companyId;
}
认证服务需要远程调用用户中心服务查询用户,在认证服务中创建Feign客户端。
@FeignClient(value = XcServiceList.XC_SERVICE_UCENTER)
public interface UserClient {
@GetMapping("/ucenter/getuserext")
public XcUserExt getUserext(@RequestParam("username") String username);
}
认证服务调用spring security接口申请令牌,spring security接口会调用UserDetailsServiceImpl从数据库查询用户,如果查询不到则返回 NULL,表示不存在;在UserDetailsServiceImpl中将正确的密码返回, spring security 会自动去比对输入密码的正确性。
修改UserDetailsServiceImpl的loadUserByUsername方法,调用Ucenter服务的查询用户接口:
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@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){
//返回NULL表示用户不存在,Spring Security会抛出异常
return null;
}
//从数据库查询用户正确的密码,Spring Security会去比对输入密码的正确性
String password = userext.getPassword();
String user_permission_string = "";
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());
return userDetails;
}
}
二、 jwt查询接口
认证服务对外提供jwt查询接口,流程如下:
1、客户端携带cookie中的身份令牌请求认证服务获取jwt 。
2、认证服务根据身份令牌从redis中查询jwt令牌并返回给客户端。
身份令牌从哪来?
该项目用jti作为用户的身份令牌。
向Oauth2申请令牌会返回上图的数据。
三、用户退出
用户退出只需要: 1、删除redis中的token。 2、删除cookie中的token。
四、Zuul网关
搭建网关工程
创建网关工程(xc-govern-gateway),注意在启动类上使用@EnableZuulProxy注解标识此工程为Zuul网关。
路由配置
客户端请求网关/api/learning,通过路由转发到/learning ;客户端请求网关/api/course,通过路由转发到/course。
身份校验
1、从cookie查询用户身份令牌是否存在,不存在则拒绝访问 。
2、从http header查询jwt令牌是否存在,不存在则拒绝访问。
3、从Redis查询user_token令牌是否过期,过期则拒绝访问。
五、方法授权
方法授权要完成的是资源服务根据jwt令牌完成对方法的授权,具体流程如下:
1、生成Jwt令牌时在令牌中写入用户所拥有的权限
我们给每个权限起个名字,例如某个用户拥有如下权限:
course_find_list:课程查询
course_pic_list:课程图片查询
2、在资源服务方法上添加注解PreAuthorize,并指定此方法所需要的权限
例如下边是课程管理接口方法的授权配置,它就表示要执行这个方法需要拥有course_find_list权限。
@PreAuthorize("hasAuthority('course_find_list')")
@Override
public QueryResult<CourseInfo> findCourseList(@PathVariable("page") int page, @PathVariable("size") int size, CourseListRequest courseListRequest)
3、当请求有权限的方法时正常访问
4、当请求没有权限的方法时则拒绝访问
jwt令牌包含权限。
动态查询用户权限:
1、管理员给用户分配权限,权限数据写到数据库中。 2、认证服务在进行用户认证时从数据库读取用户的权限数据(动态数据)。
xc_user:用户表,存储了系统用户信息,用户类型包括:学生、老师、管理员等
xc_role:角色表,存储了系统的角色信息,学生、老师、教学管理员、系统管理员等。
xc_user_role:用户角色表,一个用户可拥有多个角色,一个角色可被多个用户所拥有
xc_menu:模块表,记录了菜单及菜单下的权限
xc_permission:角色权限表,一个角色可拥有多个权限,一个权限可被多个角色所拥有
修改认证服务的UserDetailServiceImpl,查询用户的权限,并拼接权限串,代码如下:
......
//请求ucenter查询用户
XcUserExt userext = userClient.getUserext(username);
if(userext == null){
//返回NULL表示用户不存在,Spring Security会抛出异常
return null;
}
//从数据库查询用户正确的密码,Spring Security会去比对输入密码的正确性
String password = userext.getPassword();
//指定用户的权限
List<String> permissionList = new ArrayList<>();
//取出用户权限
List<XcMenu> permissions = userext.getPermissions();
for(XcMenu xcMenu:permissions){
permissionList.add(xcMenu.getCode());
}
......
Oauth2生成jwt令牌时应该会根据UserDetails生成,所以用户权限信息会包含在生成的jwt令牌里。
六、细粒度授权
什么是细粒度授权?
细粒度授权也叫数据范围授权,即不同的用户所拥有的操作权限相同,但是能够操作的数据范围是不一样的。一个例子:用户A和用户B都是教学机构,他们都拥有“我的课程”权限,但是两个用户所查询到的数据是不一样的。
本项目有哪些细粒度授权?
比如: 我的课程,教学机构只允许查询本教学机构下的课程信息。 我的选课,学生只允许查询自己所选课。
如何实现细粒度授权?
细粒度授权涉及到不同的业务逻辑,通常在service层实现,根据不同的用户进行校验,根据不同的参数查询不同的 数据或操作不同的数据。
我的课程查询,细粒度授权过程如下:
1)获取当前登录的用户Id
2)得到用户所属教育机构的Id
3)查询该教学机构下的课程信息
最终实现了用户只允许查询自己机构的课程信息。
获取当前用户
从header中取出JWT令牌,并解析JWT令牌的内容。具体方法封装成工具类Oauth2Util。代码见讲义。
七、微服务之间认证
微服务之间进行调用时需携带JWT。
可以使用Feign 拦截器。