搭建认证服务器 - Spring Security Oauth2.0 集成 Jwt 之 【密码认证流程】 总结

在搭建介绍流程之前,确保您已经搭建了一个 Eureka 注册中心,因为没有注册中心的话会报错(也有可能我搭建的认证服务器是我项目的一个子模块的原因):Request execution error. endpoint=DefaultEndpoint{ serviceUrl='http://localhost:8761/eureka/}
http://localhost:8761/eureka/ 是因为配置文件未提供注册中心地址,springcloud 默认的注册中心地址就是这个
另外:文末会提供所有代码
Oauth2.0 有以下四种授权模式:本文介绍 密码认证
1、授权码模式(Authorization Code)[常用]
2、隐式授权模式(Implicit)[不常用]
3、密码模式(Resource Owner Password Credentials)[常用]
4、客户端模式(Client Credentials)[不常用]

密码认证流程

密码模式(Resource Owner Password Credentials)与授权码模式的区别是申请令牌不再使用授权码,而是直接 通过用户名和密码即可申请令牌。

Post请求:http://localhost:9001/oauth/token

参数:

grant_type:密码模式授权填写password

username:账号

password:密码

并且此链接需要使用 http Basic认证。

所以密码认证流程相对来说比较简单的

前期工作:接下来的截图我默认你已经看过我上一篇博客 授权码认证流程,因为有些代码是在前一篇的基础上

在 UserDetailsServiceImpl 的 loadUserByUsername 方法后面
如下代码块

if(username == null){
     return null;
}
// 改成
if(username == null){
     return null;
}
String pwd = new BCryptPasswordEncoder().encode("qkm19981013");
String permissions = "permission1,permission2";
//创建User对象
return new UserJwt(username, pwd, AuthorityUtils.commaSeparatedStringToAuthorityList(permissions));

这样我们默认的账户是任意账户名,密码固定为 qkm19981013,接下来使用postman测试
在这里插入图片描述
在这里插入图片描述

基本上密码认证流程就是这样,但是这有一个问题,我们每次输入我们的用户名和密码的时候,我们都需要带上HttpBasic认证的客户端ID和密钥,但是实际上我们的ID和密钥是不允许透露出去的,这很不安全,所以接下来我们改造这种模式,客户端只需要提供用户名和密码,HttpBasic认证信息由我们服务端提供,步骤流程分为以下几步:

  1. 用户从页面输入账号密码,请求我们自己的 Controller 接口
  2. Controller 层调用 Service 层,Service 层调用 OAuth2.0 的认证地址 [/oauth/token]
  3. 进行密码授权认证操作,如果账号密码正确了,就返回令牌信息给 Service 层
  4. Service 将令牌信息给 Controller 层
  5. Controller层将数据存入到Cookie中,再响应用户.
Service 层业务
public interface AuthService {
    /**
     * 登录,授权认证方法
     */
    AuthToken login(String username, String password, String clientId, String clientSecret);
}
@Service
public class AuthServiceImpl implements AuthService {

    @Autowired
    private LoadBalancerClient loadBalancerClient;

    private RestTemplate restTemplate;

    private ClientHttpRequestFactory factory;

    @Autowired
    public void setFactory() {
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setReadTimeout(5000);//ms
        factory.setConnectTimeout(15000);//ms
        this.factory = factory;
    }

    @Autowired
    public void setRestTemplate() {
        this.restTemplate = new RestTemplate(this.factory);
    }

    @Override
    public AuthToken login(String username, String password, String clientId, String clientSecret) {
        //申请令牌
        return applyToken(username,password,clientId, clientSecret);
    }

    /**
     * 认证方法
     * @param username:用户登录名字
     * @param password:用户密码
     * @param clientId:配置文件中的客户端ID
     * @param clientSecret:配置文件中的秘钥
     * @return AuthToken
     */
    private AuthToken applyToken(String username, String password, String clientId, String clientSecret) {
        //选中认证服务的地址
        ServiceInstance serviceInstance = loadBalancerClient.choose("user-auth");
        if (serviceInstance == null) {
            throw new RuntimeException("找不到对应的服务");
        }
        //获取令牌的url
        String path = serviceInstance.getUri().toString() + "/oauth/token";
        //定义body
        MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
        //授权方式
        formData.add("grant_type", "password");
        //账号
        formData.add("username", username);
        //密码
        formData.add("password", password);
        //定义头
        MultiValueMap<String, String> header = new LinkedMultiValueMap<>();
        header.add("Authorization", httpBasic(clientId, clientSecret));
        //指定 restTemplate当遇到400或401响应时候也不要抛出异常,也要正常返回值
        restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
            @Override
            public void handleError(@NonNull ClientHttpResponse response) throws IOException {
                //当响应的值为400或401时候也要正常响应,不要抛出异常
                if (response.getRawStatusCode() != 400 && response.getRawStatusCode() != 401) {
                    super.handleError(response);
                }
            }
        });
        @SuppressWarnings("all")
        Map map;
        try {
            @SuppressWarnings("all")
            //http请求spring security的申请令牌接口
            ResponseEntity<Map> mapResponseEntity = restTemplate.exchange(path, HttpMethod.POST, new HttpEntity<>(formData, header), Map.class);
            //获取响应数据
            map = mapResponseEntity.getBody();
        } catch (RestClientException e) {
            throw new RuntimeException(e);
        }
        if (map == null || map.get("access_token") == null || map.get("refresh_token") == null || map.get("jti") == null) {
            //jti是jwt令牌的唯一标识作为用户身份令牌
            throw new RuntimeException("创建令牌失败!");
        }

        //将响应数据封装成AuthToken对象
        AuthToken authToken = new AuthToken();
        //访问令牌(jwt)
        String accessToken = (String) map.get("access_token");
        //刷新令牌(jwt)
        String refreshToken = (String) map.get("refresh_token");
        //jti,作为用户的身份标识
        String jwtToken = (String) map.get("jti");
        authToken.setJti(jwtToken);
        authToken.setAccessToken(accessToken);
        authToken.setRefreshToken(refreshToken);
        return authToken;
    }

    /**
     * base64编码
     * @param clientId 客户端ID
     * @param clientSecret 客户端密钥
     * @return String
     */
    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); //注意 "Basic " 英文后面有个空格
    }
}

//将客户端id和客户端密码拼接,按“客户端id:客户端密码” 关于为啥这样定义,且采用Base64 编码格式,如下图

我们在发送请求时,看到往请求头上加了一个头信息:Authorization

  • Basic 是明文,没有加密
  • 我们将Basic后面的密文拷贝出来,使用 Base64 解密一下看看

在这里插入图片描述

Utils 工具
@Data
public class AuthToken implements Serializable{

    /**
     * 令牌信息
     */
    String accessToken;

    /**
     * 刷新token(refresh_token)
     */
    String refreshToken;

    /**
     * jwt短令牌
     */
    String jti;

}
public class CookieUtil {

    /**
     * 设置cookie
     *
     * @param response response
     * @param name     cookie名字
     * @param value    cookie值
     * @param maxAge   cookie生命周期 以秒为单位
     */
    public static void addCookie(HttpServletResponse response, String domain, String path, String name,
                                 String value, int maxAge, boolean httpOnly) {
        Cookie cookie = new Cookie(name, value);
        cookie.setDomain(domain);
        cookie.setPath(path);
        cookie.setMaxAge(maxAge);
        cookie.setHttpOnly(httpOnly);
        response.addCookie(cookie);
    }



    /**
     * 根据cookie名称读取cookie
     * @param request request
     * @return map<cookieName,cookieValue>
     */

    public static Map<String,String> readCookie(HttpServletRequest request, String ... cookieNames) {
        Map<String,String> cookieMap = new HashMap<String,String>();
            Cookie[] cookies = request.getCookies();
            if (cookies != null) {
                for (Cookie cookie : cookies) {
                    String cookieName = cookie.getName();
                    String cookieValue = cookie.getValue();
                    for (String name : cookieNames) {
                        if (name.equals(cookieName)) {
                            cookieMap.put(cookieName, cookieValue);
                        }
                    }
                }
            }
        return cookieMap;
    }
}
/**
 * 继承了 org.springframework.security.core.userdetails.User,
 * 相当于对其的扩展,可以写我们需要添加的属性
 */
@SuppressWarnings("unused")
public class UserJwt extends User {
    private String id;    //用户ID
    private String name;  //用户名字

    public UserJwt(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, authorities);
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
VO 对象
@Data
public class LoginVo {
    @NotBlank(message = "用户名不能为空")
    private String username;

    @NotBlank(message = "密码不能为空")
    private String password;
}
Controller 层

需要的配置信息

@RestController
@RequestMapping(value = "/user")
public class AuthController {
    /**
     * 客户端ID
     */
    @Value("${auth.clientId}")
    private String clientId;

    /**
     * 秘钥
     */
    @Value("${auth.clientSecret}")
    private String clientSecret;

    /**
     * Cookie存储的域名
     */
    @Value("${auth.cookieDomain}")
    private String cookieDomain;

    /**
     * Cookie生命周期
     */
    @Value("${auth.cookieMaxAge}")
    private int cookieMaxAge;

    @Autowired
    private AuthService authService;

    @PostMapping("/login")
    public Result<Object> login(@RequestBody @Validated LoginVo loginVo) {
        //申请令牌
        AuthToken authToken;
        try {
            authToken = authService.login(loginVo.getUsername(), loginVo.getPassword(), clientId, clientSecret);
        } catch (Exception e) {
            return new Result<>(false, StatusCode.ERROR, e.getMessage());
        }
        //用户身份令牌
        String access_token = authToken.getAccessToken();
        //将令牌存储到cookie
        saveCookie(access_token);
        return new Result<>(true, StatusCode.OK, "登录成功!");
    }

    /**
     * 将令牌存储到cookie
     *
     * @param token token
     */
    private void saveCookie(String token) {
        HttpServletResponse response = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getResponse();
        assert response != null;
        CookieUtil.addCookie(response, cookieDomain, "/", "Authorization", token, cookieMaxAge, false);
    }
}

代码全部贴完了,接下来我们进行测试
在这里插入图片描述
在这里插入图片描述
再次发送请求
登录成功
查看 Cookie 信息
到此我们仅使用 用户名和密码 就进行了授权操作,保证了服务端的一定的安全性

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值