学成在线(第17天)

 用户认证

 用户认证流程分析

用户认证流程如下:

学成在线(第17天)

 业务流程说明如下:

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();
}

学成在线(第17天)

 前端显示当前用户

 需求分析

用户登录成功在页头显示当前登录的用户名称。

数据流程如下图:

学成在线(第17天)

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

学成在线(第17天)

 观察cookie是否已存入用户身份令牌。

2、get请求jwt

学成在线(第17天)

  用户退出

 需求分析

操作流程如下:
1、用户点击退出,弹出退出确认窗口,点击确定

学成在线(第17天)

 2、退出成功

学成在线(第17天)

用户退出要以下动作:
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的作用是保障微服
务的安全访问,拦截微服务请求,校验合法性及负载均衡。

学成在线(第17天)

 路由配置

需求分析

Zuul网关具有代理的功能,根据请求的url转发到微服务,如下图:

学成在线(第17天)

客户端请求网关/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:可以设置过虑的头信息,默认为空表示不过虑任何头

学成在线(第17天)

 完整的路由配置

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查询课程信息

学成在线(第17天)

相关推荐