认证开发
需求分析
功能流程图如下:
执行流程:
1、用户登录,请求认证服务 2、认证服务认证通过,生成jwt令牌,将jwt令牌及相关信息写入Redis,并且将身份令牌写入cookie 3、用户访问资源页面,带着cookie到网关 4、网关从cookie获取token,并查询Redis校验token,如果token不存在则拒绝访问,否则放行 5、用户退出,请求认证服务,清除redis中的token,并且删除cookie中的token
使用redis存储用户的身份令牌有以下作用:
1、实现用户退出注销功能,服务端清除令牌后,即使客户端请求携带token也是无效的。
2、由于jwt令牌过长,不宜存储在cookie中,所以将jwt令牌存储在redis,由客户端请求服务端获取并在客户端存储。
Redis配置
将认证服务changgou_user_auth中的application.yml配置文件中的Redis配置改成自己对应的端口和密码。
认证服务
认证需求分析
认证服务需要实现的功能如下:
1、登录接口
前端post提交账号、密码等,用户身份校验通过,生成令牌,并将令牌存储到redis。 将令牌写入cookie。
2、退出接口 校验当前用户的身份为合法并且为已登录状态。 将令牌从redis删除。 删除cookie中的令牌。
授权参数配置
修改changgou_user_auth中application.yml配置文件,修改对应的授权配置
auth: ttl: 1200 #token存储到redis的过期时间 clientId: changgou #客户端ID clientSecret: changgou #客户端秘钥 cookieDomain: localhost #Cookie保存对应的域名 cookieMaxAge: -1 #Cookie过期时间,-1表示浏览器关闭则销毁
申请令牌测试
为了不破坏Spring Security的代码,我们在Service方法中通过RestTemplate请求Spring Security所暴露的申请令 牌接口来申请令牌,下边是测试代码:
@SpringBootTest @RunWith(SpringRunner.class) public class TokenTest { @Autowired private LoadBalancerClient loadBalancerClient; @Autowired private RestTemplate restTemplate; /**** * 发送Http请求创建令牌 */ @Test public void testCreateToken() throws InterruptedException { //采用客户端负载均衡,从eureka获取认证服务的ip 和端口 ServiceInstance serviceInstance = loadBalancerClient.choose("USER-AUTH"); URI uri = serviceInstance.getUri(); //申请令牌地址 String authUrl = uri + "/oauth/token"; //1、header信息,包括了http basic认证信息 MultiValueMap<String, String> headers = new LinkedMultiValueMap<String, String>(); //进行Base64编码,并将编码后的认证数据放到头文件中 String httpbasic = httpbasic("changgou", "changgou"); headers.add("Authorization", httpbasic); //2、指定认证类型、账号、密码 MultiValueMap<String, String> body = new LinkedMultiValueMap<String, String>(); body.add("grant_type","password"); body.add("username","itheima"); body.add("password","123456"); HttpEntity<MultiValueMap<String, String>> multiValueMapHttpEntity = new HttpEntity<MultiValueMap<String, String>>(body, headers); //指定 restTemplate当遇到400或401响应时候也不要抛出异常,也要正常返回值 restTemplate.setErrorHandler(new DefaultResponseErrorHandler() { @Override public void handleError(ClientHttpResponse response) throws IOException { //当响应的值为400或401时候也要正常响应,不要抛出异常 if (response.getRawStatusCode() != 400 && response.getRawStatusCode() != 401) { super.handleError(response); } } }); //远程调用申请令牌 ResponseEntity<Map> exchange = restTemplate.exchange(authUrl, HttpMethod.POST, multiValueMapHttpEntity, Map.class); Map result = exchange.getBody(); System.out.println(result); } /*** * base64编码 * @param clientId * @param clientSecret * @return */ private String httpbasic(String clientId,String clientSecret){ //将客户端id和客户端密码拼接,按“客户端id:客户端密码” String string = clientId+":"+clientSecret; //进行base64编码 byte[] encode = Base64Utils.encode(string.getBytes()); return "Basic "+new String(encode); } }
业务层
AuthService接口:
public interface AuthService { AuthToken login(String username, String password, String clientId, String clientSecret); }
AuthServiceImpl实现类:
基于刚才写的测试实现申请令牌的service方法如下:
@Service public class AuthServiceImpl implements AuthService { @Autowired private RestTemplate restTemplate; @Autowired private LoadBalancerClient loadBalancerClient; @Autowired private StringRedisTemplate stringRedisTemplate; @Value("${auth.ttl}") private long ttl; /** * 申请令牌 * @param username * @param password * @param clientId * @param clientSecret * @return */ @Override public AuthToken applyToken(String username, String password, String clientId, String clientSecret) { ServiceInstance serviceInstance = loadBalancerClient.choose("user-auth"); URI uri = serviceInstance.getUri(); String url = uri+"/oauth/token"; MultiValueMap<String, String> body = new LinkedMultiValueMap<>(); body.add("grant_type","password"); body.add("username",username); body.add("password",password); MultiValueMap<String, String> headers = new LinkedMultiValueMap<>(); headers.add("Authorization",this.getHttpBasic(clientId,clientSecret)); HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(body,headers); restTemplate.setErrorHandler(new DefaultResponseErrorHandler(){ @Override public void handleError(ClientHttpResponse response) throws IOException { if (response.getRawStatusCode()!=400 && response.getRawStatusCode()!=401){ super.handleError(response); } } }); ResponseEntity<Map> responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, Map.class); Map map = responseEntity.getBody(); if (map==null || map.get("access_token")==null || map.get("refresh_token")==null || map.get("jti")==null){ throw new RuntimeException("申请令牌失败"); } AuthToken authToken = new AuthToken(); authToken.setAccessToken((String) map.get("access_token")); authToken.setRefreshToken((String) map.get("refresh_token")); authToken.setJti((String) map.get("jti")); stringRedisTemplate.boundValueOps(authToken.getJti()).set(authToken.getAccessToken(),ttl, TimeUnit.SECONDS); return authToken; } private String getHttpBasic(String clientId, String clientSecret) { String value = clientId+":"+clientSecret; byte[] encode = Base64Utils.encode(value.getBytes()); return "Basic "+new String(encode); } }
控制层
AuthController编写用户登录授权方法,代码如下:
@RestController @RequestMapping("/oauth") public class AuthController { @Autowired private AuthService authService; @Value("${auth.clientId}") private String clientId; @Value("${auth.clientSecret}") private String clientSecret; @Value("${auth.cookieDomain}") private String cookieDomain; @Value("${auth.cookieMaxAge}") private int cookieMaxAge; @PostMapping("/login") public Result login(String username,String password){ if (StringUtils.isEmpty(username)){ throw new RuntimeException("用户名不存在"); } if (StringUtils.isEmpty(password)){ throw new RuntimeException("密码不存在"); } AuthToken authToken = authService.applyToken(username,password,clientId,clientSecret); this.saveJtiToCookie(authToken.getJti()); return new Result(true, StatusCode.OK,"登录成功"); } private void saveJtiToCookie(String jti) { HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse(); CookieUtil.addCookie(response,cookieDomain,"/","uid",jti,cookieMaxAge,false); } }
登录请求放行
修改认证服务WebSecurityConfig类中configure(),添加放行路径
测试认证接口
使用postman测试:
1)Post请求:http://localhost:9200/oauth/login
动态获取用户信息
当前在认证服务中,用户密码是写死在用户认证类中。所以用户登录时,无论帐号输入什么,只要密码是itheima都可以访问。 因此需要动态获取用户帐号与密码.
定义被访问接口
用户微服务对外暴露根据用户名获取用户信息接口
@GetMapping("/load/{username}") public User findUserInfo(@PathVariable("username") String username){ return userService.findById(username); }
放行该接口,修改ResourceServerConfig类
定义feign接口
changgou_user_server_api新增feign接口
@FeignClient(name="user") public interface UserFeign { @GetMapping("/user/load/{username}") public User findUserInfo(@PathVariable("username") String username); }
认证服务添加依赖
<dependency> <groupId>com.changgou</groupId> <artifactId>changgou_service_user_api</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
修改认证服务启动类
@EnableFeignClients(basePackages = "com.changgou.user.feign")
修改用户认证类
测试: 重新启动服务并申请令牌