学成在线(第17天)用户认证 Zuul
用户认证
用户认证流程分析
用户认证流程如下:
业务流程说明如下:
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令牌,完成授权,拥有权限的方法正常执行,没有权限的方法将拒绝访问。
查询用户接口
Api接口
用户中心对外提供如下接口:
1、响应数据类型
此接口将来被用来查询用户信息及用户权限信息,所以这里定义扩展类型
@Data @ToString public class XcUserExt extends XcUser { //权限信息 private List<XcMenu> permissions; //企业信息 private String companyId; }
2、根据账号查询用户信息
@Api(value = "用户中心",description = "用户中心管理") public interface UcenterControllerApi { public XcUserExt getUserext(String username); }
DAO
添加XcUser、XcCompantUser两个表的Dao
public interface XcUserRepository extends JpaRepository<XcUser, String> { XcUser findXcUserByUsername(String username); } public interface XcCompanyUserRepository extends JpaRepository<XcCompanyUser,String> { //根据用户id查询所属企业id XcCompanyUser findByUserId(String userId); }
Service
@Service public class UserService { @Autowired private XcUserRepository xcUserRepository; //根据用户账号查询用户信息 public XcUser findXcUserByUsername(String username){ return xcUserRepository.findXcUserByUsername(username); } //根据账号查询用户的信息,返回用户扩展信息 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 userId = xcUserExt.getId(); //查询用户所属公司 XcCompanyUser xcCompanyUser = xcCompanyUserRepository.findXcCompanyUserByUserId(userId); if(xcCompanyUser!=null){ String companyId = xcCompanyUser.getCompanyId(); xcUserExt.setCompanyId(companyId); } return xcUserExt; } }
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; } }
调用查询用户接口
创建client
认证服务需要远程调用用户中心服务查询用户,在认证服务中创建Feign客户端
@FeignClient(value = XcServiceList.XC_SERVICE_UCENTER) public interface UserClient { @GetMapping("/ucenter/getuserext") public XcUserExt getUserext(@RequestParam("username") String username) }
UserDetailsServiceImpl
认证服务调用spring security接口申请令牌,spring security接口会调用UserDetailsServiceImpl从数据库查询用
户,如果查询不到则返回 NULL,表示不存在;在UserDetailsServiceImpl中将正确的密码返回, spring security
会自动去比对输入密码的正确性。
1、修改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; } }
BCryptPasswordEncoder
早期使用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
//采用bcrypt对密码进行Hash @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
前端显示当前用户
需求分析
用户登录成功在页头显示当前登录的用户名称。
数据流程如下图:
1、用户请求认证服务,登录成功。
2、用户登录成功,认证服务向cookie写入身份令牌,向redis写入user_token(身份令牌及授权jwt授权令牌)
3、客户端携带cookie中的身份令牌请求认证服务获取jwt令牌。
4、客户端解析jwt令牌,并将解析的用户信息存储到sessionStorage中。
jwt令牌中包括了用户的基本信息,客户端解析jwt令牌即可获取用户信息。
5、客户端从sessionStorage中读取用户信息,并在页头显示。
jwt 查询接口
需求分析
认证服务对外提供jwt查询接口,流程如下:
1 、客户端携带cookie中的身份令牌请求认证服务获取jwt
2、认证服务根据身份令牌从redis中查询jwt令牌并返回给客户端。
API
在认证模块定义 jwt查询接口:
@Api(value = "jwt查询接口",description = "客户端查询jwt令牌内容") public interface AuthControllerApi { @ApiOperation("查询userjwt令牌") public JwtResult userjwt();
Service
在AuthService中定义方法如下:
//从redis查询令牌 public AuthToken getUserToken(String token){ String userToken = "user_token:"+token; String userTokenString = stringRedisTemplate.opsForValue().get(userToken); if(userToken!=null){ AuthToken authToken = null; try { authToken = JSON.parseObject(userTokenString, AuthToken.class); } catch (Exception e) { LOGGER.error("getUserToken from redis and execute JSON.parseObject error {}",e.getMessage()); e.printStackTrace(); } return authToken; } return null; }
Controller
@Override @GetMapping("/userjwt") public JwtResult userjwt() { //获取cookie中的令牌 String access_token = getTokenFormCookie(); //根据令牌从redis查询jwt AuthToken authToken = authService.getUserToken(access_token); if(authToken == null){ return new JwtResult(CommonCode.FAIL,null); } return new JwtResult(CommonCode.SUCCESS,authToken.getJwt_token()); } //从cookie中读取访问令牌 private String getTokenFormCookie(){ Map<String, String> cookieMap = CookieUtil.readCookie(request, "uid"); String access_token = cookieMap.get("uid"); return access_token; }
测试
使用postman测试
1、请求 /auth/userlogin
观察cookie是否已存入用户身份令牌。
2、get请求jwt
用户退出
需求分析
操作流程如下:
1、用户点击退出,弹出退出确认窗口,点击确定
2、退出成功
用户退出要以下动作:
1、删除redis中的token
2、删除cookie中的token
API
认证服务对外提供退出接口。
@ApiOperation("退出") public ResponseResult logout();
服务端
认证服务提供退出接口。
Service
//从redis中删除令牌 public boolean delToken(String access_token){ String name = "user_token:" + access_token; stringRedisTemplate.delete(name); return true; }
Controller
//退出 @Override @PostMapping("/userlogout") public ResponseResult logout() { //取出身份令牌 String uid = getTokenFormCookie(); //删除redis中token authService.delToken(uid); //清除cookie clearCookie(uid); return new ResponseResult(CommonCode.SUCCESS); } //清除cookie private void clearCookie(String token){ CookieUtil.addCookie(response, cookieDomain, "/", "uid", token, 0, false); }
退出URL放行
认证服务默认都要校验用户的身份信息,这里需要将退出url放行。
在WebSecurityConfig类中重写 configure(WebSecurity web)方法,如下:
@Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/userlogin","/userlogout"); }
Zuul 网关
Zuul 介绍
什么是Zuul?
Spring Cloud Zuul是整合Netflix公司的Zuul开源项目实现的微服务网关,它实现了请求路由、负载均衡、校验过
虑等 功能。
什么是网关?
服务网关是在微服务前边设置一道屏障,请求先到服务网关,网关会对请求进行过虑、校验、路由等处理。有了服
务网关可以提高微服务的安全性,网关校验请求的合法性,请求不合法将被拦截,拒绝访问。
Zuul与Nginx怎么配合使用?
Zuul与Nginx在实际项目中需要配合使用,如下图,Nginx的作用是反向代理、负载均衡,Zuul的作用是保障微服
务的安全访问,拦截微服务请求,校验合法性及负载均衡。
路由配置
需求分析
Zuul网关具有代理的功能,根据请求的url转发到微服务,如下图:
客户端请求网关/api/learning,通过路由转发到/learning
客户端请求网关/api/course,通过路由转发到/course
路由配置
在appcation.yml中配置:
zuul: routes: manage‐course: #路由名称,名称任意,保持所有路由名称唯一 path: /course/** serviceId: xc‐service‐manage‐course #指定服务id,从Eureka中找到服务的ip和端口 #url: http://localhost:31200 #也可指定url strip‐prefix: false #true:代理转发时去掉前缀,false:代理转发时不去掉前缀 sensitiveHeaders: #默认zuul会屏蔽cookie,cookie不会传到下游服务,这里设置为空则取消默认的黑名 单,如果设置了具体的头信息则不会传到下游服务 # ignoredHeaders: Authorization
serviceId:推荐使用serviceId,zuul会从Eureka中找到服务id对应的ip和端口。
strip-prefix: false #true:代理转发时去掉前缀,false:代理转发时不去掉前缀,例如,为true请
求/course/coursebase/get/..,代理转发到/coursebase/get/,如果为false则代理转发到/course/coursebase/get
sensitiveHeaders :敏感头设置,默认会过虑掉cookie,这里设置为空表示不过虑
ignoredHeaders:可以设置过虑的头信息,默认为空表示不过虑任何头
完整的路由配置
zuul: routes: xc‐service‐learning: #路由名称,名称任意,保持所有路由名称唯一 path: /learning/** serviceId: xc‐service‐learning #指定服务id,从Eureka中找到服务的ip和端口 strip‐prefix: false sensitiveHeaders: manage‐course: path: /course/** serviceId: xc‐service‐manage‐course strip‐prefix: false sensitiveHeaders: manage‐cms: path: /cms/** serviceId: xc‐service‐manage‐cms strip‐prefix: false sensitiveHeaders: manage‐sys: path: /sys/** serviceId: xc‐service‐manage‐cms strip‐prefix: false sensitiveHeaders: service‐ucenter: path: /ucenter/** serviceId: xc‐service‐ucenter sensitiveHeaders: strip‐prefix: false xc‐service‐manage‐order: path: /order/** serviceId: xc‐service‐manage‐order sensitiveHeaders: strip‐prefix: false
过滤器
Zuul的核心就是过虑器,通过过虑器实现请求过虑,身份校验等。
ZuulFilter
自定义过虑器需要继承 ZuulFilter,ZuulFilter是一个抽象类,需要覆盖它的四个方法,如下:
1、 shouldFilter:返回一个Boolean值,判断该过滤器是否需要执行。返回true表示要执行此过虑器,否则不执
行。 2、 run:过滤器的业务逻辑。 3、 filterType:返回字符串代表过滤器的类型,如下 pre:请求在被路由之前
执行 routing:在路由请求时调用 post:在routing和errror过滤器之后调用 error:处理请求时发生错误调用
4、 filterOrder:此方法返回整型数值,通过此数值来定义过滤器的执行顺序,数字越小优先级越高。
测试
过虑所有请求,判断头部信息是否有Authorization,如果没有则拒绝访问,否则转发到微服务。
定义过虑器,使用@Component标识为bean。
@Component public class LoginFilterTest extends ZuulFilter { private static final Logger LOG = LoggerFactory.getLogger(LoginFilterTest.class); @Override public String filterType() { return "pre"; } @Override public int filterOrder() { return 2;//int值来定义过滤器的执行顺序,数值越小优先级越高 } @Override public boolean shouldFilter() {// 该过滤器需要执行 return true; } @Override public Object run() { RequestContext requestContext = RequestContext.getCurrentContext(); HttpServletResponse response = requestContext.getResponse(); HttpServletRequest request = requestContext.getRequest(); //取出头部信息Authorization String authorization = request.getHeader("Authorization"); if(StringUtils.isEmpty(authorization)){ requestContext.setSendZuulResponse(false);// 拒绝访问 requestContext.setResponseStatusCode(200);// 设置响应状态码 ResponseResult unauthenticated = new ResponseResult(CommonCode.UNAUTHENTICATED); String jsonString = JSON.toJSONString(unauthenticated); requestContext.setResponseBody(jsonString); requestContext.getResponse().setContentType("application/json;charset=UTF‐8"); return null; } return null; } }
测试:
请求:http://localhost:50201/api/course/coursebase/get/4028e581617f945f01617f9dabc40000查询课程信息