畅购商城(九):Spring Security Oauth2


好好学习,天天向上

本文已收录至我的Github仓库DayDayUP:github.com/RobodLee/DayDayUP,欢迎Star,更多文章请前往:目录导航

  • 畅购商城(一):环境搭建
  • 畅购商城(二):分布式文件系统FastDFS
  • 畅购商城(三):商品管理
  • 畅购商城(四):Lua、OpenResty、Canal实现广告缓存与同步
  • 畅购商城(五):Elasticsearch实现商品搜索
  • 畅购商城(六):商品搜索
  • 畅购商城(七):Thymeleaf实现静态页
  • 畅购商城(八):微服务网关和JWT令牌
  • 畅购商城(九):Spring Security Oauth2
  • 畅购商城(十):购物车
  • 畅购商城(十一):订单
  • 畅购商城(十二):接入微信支付
  • 畅购商城(十三):秒杀系统「上」
  • 畅购商城(十四):秒杀系统「下」

前言

之前因为没学过Spring Security和OAuth2.0,所以看这一章的视频的时候看的一头雾水。所以花了几天时间恶补了一下这方面的知识,并且写了两篇文章,把这两部分内容详细说明了一下。

  • SpringBoot整合Spring Security

  • OAuth2.0分布式系统环境搭建

下面的内容只是针对于这个项目的,前两篇文章中说过的内容就不再说了。

认证服务介绍

怎么搭建OAuth2.0我之前的文章已经详细说过了,这里直接将资料里提供的代码导入即可。

image

这里简单总结一下每个文件的作用:

  • AuthorizationServerConfig

    这个是OAuth2.0的认证服务配置。主要有三点,第一是客户端信息配置,也就是客户端需要有哪些条件才可以访问服务器,比如客户端id和客户端密钥等,可以直接配置到内存中,也可以配置从数据库中读取;第二是授权服务器端点配置,就是配置认证管理器,令牌存储方式等;第三个是授权服务器的安全配置,就是配置访问的限制,比如限制校验令牌的配置等。

  • CustomUserAuthenticationConverter

    自定义的UserAuthenticationConverter,继承自DefaultUserAuthenticationConverter,重写了convertUserAuthentication方法。默认该方法是获取authentication中的username权限信息。而我们重写的方法里面还获取了authentication中的principal,判断是不是我们自定义的UserJwt,不是的话就调用userDetailsService.loadUserByUsername去获取,然后将userJwt中的nameid获取出来,添加到返回的map中。

  • UserDetailsServiceImpl

    这个是自定义的认证授权类,实现了UserDetailsService接口,并实现了里面的loadUserByUsername()方法。这个方法是根据前端传进来的用户名去查出对应的用户信息。然后交给后续的过滤器去进行用户身份的验证。一般这个方法是从数据库中查找用户,但是这里为了测试就直接new了一个临时用户,密码是 "robod666" ,所以只要前端传过来的密码是 “robod666” 就可以正常登录。

  • WebSecurityConfig

    这个是Spring Security的安全配置类。主要配置了某些对于某些请求的限制。在这个类中,还往Spring容器中注入了passwordEncoderauthenticationManagerBean供其他类使用。

  • UserLoginController

    这个是自定义的一个只使用用户名和密码进行登录的简化的登录方式。

  • LoginServiceLoginServiceImpl

    UserLoginController的Service层。负责添加一些必要的信息后然后通过RestTemplate模拟浏览器向服务器发送请求获取令牌信息。

  • AuthToken

    封装了Token的相关信息。令牌信息,刷新token,jwt短令牌。

  • CookieUtil

    Cookie的工具类。设置Cookie以及根据名称获取Cookie信息。

  • UserJwt

    用户信息。实现了UserDetails接口。验证用户时用的就是这个类的对象。

  • changgou.jks

    密钥证书,可以使用keytool工具生成。

  • application.yml

    认证服务的配置文件

    …………
    # 配置信息,给UserLoginController用的
    auth:
      ttl: 3600  #token存储到redis的过期时间
      clientId: changgou
      clientSecret: changgou
      cookieDomain: localhost
      cookieMaxAge: -1
    
    # 因为采用了非对称加密,所以这里配置了密钥的相关信息
    encrypt:
      key-store:
        location: classpath:/robod666.jks
        secret: robod666
        alias: robod666
        password: robod666
        …………
    

这几个文件的作用到这里就介绍完了。

非对称加密认证

认证流程分析

image

这个是传统的认证流程,当我们携带令牌去访问资源服务器的时候,资源服务会将令牌发送到授权服务中验证令牌时候合法。这样做的话无形之中增加的服务器的压力,因为多了一次服务器之间交互的行为,效率低下。

为了提高效率,采用了公钥私钥验证的方式。

image

授权服务采用私钥去生成令牌,然后客户端携带令牌向资源服务器发送请求。资源服务器采用公钥对令牌进行校验,校验通过后再进行下一步操作。减少了和授权服务的交互。

公钥可以保存在任意的服务器中,但是私钥只能保存在授权服务中。因为有了私钥后就可以去伪造令牌,降低了安全性。所以采用非对称加密也是为了提高安全性。

生成密钥证书

我们可以使用keytool工具来生成密钥对。在准备好的文件夹下,打开命令行窗口,执行以下内容:

keytool -genkeypair -alias robod666 -keyalg RSA -keypass robod666 -keystore robod666.jks -storepass robod666
# -alias:密钥的别名 
# -keyalg:使用的hash算法 
# -keypass:密钥的访问密码
# -keystore:密钥库文件名,xc.keystore保存了生成的证书 
# -storepass:密钥库的访问密码 

然后界面上会出现几个问题,答案随便输,最后输入 “y” 即可生成密钥。

image

把生成的密钥证书放在认证服务的resources目录下即可。

提取公钥

在安装好openssl后,在密钥证书所在的目录下打开命令行窗口,执行

keytool -list -rfc --keystore robod666.jks | openssl x509 -inform pem -pubkey

image

这样就可以将公钥提取出来。在需要使用到公钥的微服务的resources目录下创建一个public.key的文件,把这段内容合并为一行粘贴进去。

创建及解析令牌

public class JWTTest {

    //创建令牌
    @Test
    public void createJWT() {
        ClassPathResource classPathResource = new ClassPathResource("robod666.jks");
        KeyStoreKeyFactory keyStoreKeyFactory = 
            new KeyStoreKeyFactory(classPathResource, "robod666".toCharArray());
        KeyPair keyPair = keyStoreKeyFactory.getKeyPair("robod666");
        PrivateKey privateKey = keyPair.getPrivate();
        Map tokenMap = new HashMap<>();
        tokenMap.put("id", "1");
        tokenMap.put("name", "robod");
        tokenMap.put("roles", "ROLE_VIP,ROLE_USER");
        Jwt jwt = JwtHelper.encode(JSON.toJSONString(tokenMap), new RsaSigner((RSAPrivateKey) privateKey));
        System.out.println(jwt.getEncoded());
    }

    //解析令牌
    @Test
    public void parseJWT() {
        String token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6IlJPTEVfVklQLFJPTEVfVVNFUiIsIm5hbWUiOiJyb2JvZCIsImlkIjoiMSJ9.KkPXXlYTkDCWq2VN5qy6w6FI5TgCbIy-GkQShaYGbfvcZsj0165XsgXSx7Sf5aUpGB-494ds-ZnLxs3oVMZ7_tbu-is1-gZOeQ0G1GLla0ytNkImXabnujgWH2B4bmX4lBLK7d8xTEQ4WoAnydWUusCmPjQDgdFZGHmccJLuYKqQPzru-4go1mFgjEeB7gNu6cLYyQc79bZdF2Mk2OX1Nxpb88sux0QkNAlb1-JuUhmjbUYwMK5l5W5zeNckRJtGy_Zy7OTwXviuRp6uISmWD7p1HYbkKH-ROKCgSu1cqnok0645Uou7Y54Nd8NosIqShuNYbBBo_BHWuyx_lKdk1g";
        String publicKey = "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAiq6KfbXc/viuB6oQ/80cfLSFIr7pX3PmteAQ2/dA+ReMLgULJb+U8Dax3xNpBgLAp+Ei2IMkBFJlJRn/iaYi5eMnCY2vyfHkC69x6OhhCtzWBRxGJkPRjLDU+Obhak2MrDI4zIpzQs2/phjqWXuEPMz7KMd5UhoAFZWLTW1Ih3CP962fuJdV83hj/2uWN/yaAgaLRxRlTw7HHoIEy1dX9prAnqQ/rOl2Igvwi23GNnzMrqlvR9qt1gBI+noHtMv07hkavUT1nmoYnt/pw2+FLMLFEun2gR3DUmqu79QC6trDf3cVfKyRP9A7TBjUEv+Ecrh8JQosQa8GongTzHhmOwIDAQAB-----END PUBLIC KEY-----";
        Jwt jwt = JwtHelper.decodeAndVerify(token, new RsaVerifier(publicKey));
        String claims = jwt.getClaims();
        System.out.println(claims);
    }
}

创建令牌的时候,可能会出现以下 java.lang.IllegalStateException 错误,

image

把idea重启一下即可。

运行createJWT()成功的话,就会出现以下内容。

image

这就是我们创建的令牌了。

运行parseJWT(),运行成功的话,就可以使用公钥去解析出令牌中的内容。

image

账号密码登录

现在我们登录的时候,除了需要用户名密码之外,还需要指定clientId等其它信息。那我们可不可以只使用账号密码就能登录呢?只需要在服务器中将其它的信息事先指定好即可。思路就是,前端将用户名密码传入进来,然后服务器端添加好其它的信息,发送给认证服务去登录,然后拿到token再返回给前端。

@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private LoadBalancerClient loadBalancerClient;

    @Override
    public Map login(String username, String password, String clientId, String clientSecret, String grantType) {
        String url = loadBalancerClient.choose("user-auth").getUri() +
                "/oauth/token";
        //请求体
        MultiValueMap body = new LinkedMultiValueMap<>();
        body.add("username", username);
        body.add("password", password);
        body.add("grant_type", grantType);
        //请求头
        MultiValueMap header = new LinkedMultiValueMap<>();
        String authorization = "Basic " +
                new String(Base64.getEncoder().encode((clientId + ":" + clientSecret).getBytes()));
        header.add("Authorization", authorization);
        HttpEntity httpEntity = new HttpEntity(body, header);
        ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, httpEntity, Map.class);
        return response.getBody();
    }

}
@Value("${auth.clientId}")
private String clientId;

@Value("${auth.clientSecret}")
private String clientSecret;

//密码模式  认证.
@RequestMapping("/login")
public Result login(String username, String password) throws Exception {
    String grantType = "password";
    Map token = loginService.login(username, password, clientId, clientSecret, grantType);
    return new Result<>(true, StatusCode.OK,"令牌生成成功",token);
}

image

好了,现在我们只用账号密码就可以申请到令牌了。

总结

这篇文章主要是说了如何导入OAuth2.0认证服务,如果不了解的话,理解起来还是很困难的,所以可以先去看看我之前的文章,导入之后对每个文件的作用进行了一个简单的介绍。然后又讲了一下采用非对称加密的时候如何生成公钥私钥。最后说了如何实现只用账号密码进行登录。

如果我的文章对你有些帮助,不要忘了点赞收藏转发关注。要是有什么好的意见欢迎在下方留言。让我们下期再见!

微信公众号

相关