Loading

认证开发

需求分析

功能流程图如下:

执行流程:

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")
修改用户认证类

测试: 重新启动服务并申请令牌

posted @ 2021-08-10 13:09  1640808365  阅读(93)  评论(0编辑  收藏  举报