SpringSecurity+Oauth2+jwt 非对称加密实现单点登录

1、相关概念简单说明及总流程图

  • 非对称加密算法概念

    在这里插入图片描述

  • jwt说明
    JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于 在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公 钥/私钥对来签名,防止被篡改。
    jwt特性:
    * 基于json,容易解析
    * 可以在令牌中自定义内容,容易扩展
    * 通过非对称加密算法以及签名技术,有效防止jwt被篡改
    * 在不通过认证服务器的情况下可以通过资源服务器完成认证
    * jwt令牌较长,占用存储空间

    jwt令牌结构
    * Header: 包括了令牌类型以及所使用的的算法
    * Payload : 存放了一些有效信息,以及自己添加的一些信息,但是不包括一些敏感的信息。
    * signature: 签名部分,防止被篡改
    完整形式:
    {
    alg: “RS256”,
    typ: “JWT”
    }.
    {
    companyId: “1”,
    userpic: null,
    user_name: “sss”,
    scope: [
    “app”
    ],
    name: “test02”,
    utype: “101002”,
    id: “49”,
    exp: 1593981062,
    authorities: [
    “course_view”,
    ],
    jti: “aba19576-8e73-430a-81cc-5c87d73012d2”,
    client_id: “ddd”
    }.
    [signature]
    Oauth2说名
    Oauth2的详细说明

  • 总体流程图
    在从这里插入图片描述

2、相关的配置

Pom文件:

 <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>

Application.yml文件

#自定义配置
auth:
  tokenValiditySeconds: 1200  #token存储到redis的过期时间
  clientId: XcWebApp
  clientSecret: XcWebApp
  cookieDomain: xuecheng.com
  cookieMaxAge: -1

# 鉴权服务器配置
encrypt:
  key-store:
    location: classpath:/xc.keystore
    secret: xuechengkeystore
    alias: xckey
    password: xuecheng
#资源服务器配置
security:
  oauth2:
    client:
      access-token-validity-seconds: 1200
      client-id: XcWebApp
      client-secret:  XcWebApp

3、认证服务器配置

package cn.hegongda.xuecheng.auth.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.bootstrap.encrypt.KeyProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.security.KeyPair;


@Configuration
@EnableAuthorizationServer
class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private DataSource dataSource;
    //jwt令牌转换器
    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;
    @Autowired
    UserDetailsService userDetailsService;
    @Autowired
    AuthenticationManager authenticationManager;
    @Autowired
    TokenStore tokenStore;
    @Autowired
    private CustomUserAuthenticationConverter customUserAuthenticationConverter;

    //读取密钥的配置
    @Bean("keyProp")
    public KeyProperties keyProperties(){

        return new KeyProperties();
    }

    @Resource(name = "keyProp")
    private KeyProperties keyProperties;


    //客户端配置
    @Bean
    public ClientDetailsService clientDetails() {

        return new JdbcClientDetailsService(dataSource);
    }
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.jdbc(this.dataSource).clients(this.clientDetails());
      
  
    @Bean
    @Autowired
    public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
        return new JwtTokenStore(jwtAccessTokenConverter);
    }
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(CustomUserAuthenticationConverter customUserAuthenticationConverter) {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        KeyPair keyPair = new KeyStoreKeyFactory
                (keyProperties.getKeyStore().getLocation(), keyProperties.getKeyStore().getSecret().toCharArray())
                .getKeyPair(keyProperties.getKeyStore().getAlias(),keyProperties.getKeyStore().getPassword().toCharArray());
        converter.setKeyPair(keyPair);
    
        return converter;
    }
    //授权服务器端点配置
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

        endpoints.accessTokenConverter(jwtAccessTokenConverter)
                .authenticationManager(authenticationManager)//认证管理器
                .tokenStore(tokenStore)//令牌存储
                .userDetailsService(userDetailsService);//用户信息service
    }

    //授权服务器的安全配置
    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
//        oauthServer.checkTokenAccess("isAuthenticated()");//校验token需要认证通过,可采用http basic认证
        oauthServer.allowFormAuthenticationForClients()
                .passwordEncoder(new BCryptPasswordEncoder())
                // 开启/oauth/token_key验证端口无权限访问
                .tokenKeyAccess("permitAll()")
                // 开启/oauth/check_token验证端口认证权限访问
                .checkTokenAccess("isAuthenticated()");
    }



}


@Configuration
@EnableWebSecurity
@Order(-1)
class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/userlogin","/userlogout","/userjwt");

    }
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        AuthenticationManager manager = super.authenticationManagerBean();
        return manager;
    }
    //采用bcrypt对密码进行编码
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .httpBasic().and()
                .formLogin()
                .and()
                .authorizeRequests().anyRequest().authenticated();

    }
}

4、登陆认证申请令牌

 @Autowired
    private AuthService authService;

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

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

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

    @Value("${auth.cookieMaxAge}")
    int cookieMaxAge;

    @PostMapping("/userlogin")
    @Override
    public LoginResult login(LoginRequest loginRequest) {

        if (loginRequest == null || StringUtils.isEmpty(loginRequest.getUsername())) {
            ExceptionCast.cast(AuthCode.AUTH_ACCOUNT_NOTEXISTS);
        }

        if (loginRequest == null || StringUtils.isEmpty(loginRequest.getPassword())) {
            ExceptionCast.cast(AuthCode.AUTH_PASSWORD_NONE);
        }

        String password = loginRequest.getPassword();
        String username = loginRequest.getUsername();

        // 申请令牌
        AuthToken authToken = authService.getAuthToken(username,password,clientId,clientSecret);

        // 将获取的authToken存入cookie中
        saveAuthTokenToCookie(authToken.getAccess_token());

        return new LoginResult(CommonCode.SUCCESS,authToken.getAccess_token());
    }

  /**
    将accessToken的唯一标识存放到浏览器cookie中,指定域名携带该cookie
  */
    private void saveAuthTokenToCookie(String access_token){
        HttpServletResponse response = ((ServletRequestAttributes)                          RequestContextHolder.getRequestAttributes()).getResponse();
       CookieUtil.addCookie(response,cookieDomain,"/","uid",access_token,cookieMaxAge,false);
    }

申请令牌代码:

@Autowired
    private RestTemplate restTemplate;

    // 获取申请令牌的地址
    @Autowired
    private LoadBalancerClient loadBalancerClient;

    @Autowired
    private RedisTemplate redisTemplate;

    @Value("${auth.tokenValiditySeconds}")
    Long tokenExpireTime;


    // 用户认证申请令牌
    public AuthToken getAuthToken(String username, String password, String clientId, String clientSecret) {

        AuthToken authToken = applyToken(username,password,clientId,clientSecret);

        // 判断令牌是否申请成功
        if (authToken == null) {
            ExceptionCast.cast(AuthCode.AUTH_TOKEN_GET_FAIL);
        }
        // 将令牌存入到redis中,并设置其过期时间
        boolean b = saveTokenToRedis(authToken,tokenExpireTime);
        if (!b){
            ExceptionCast.cast(AuthCode.AUTH_TOKEN_SAVE_FAIL);
        }
        return authToken;
    }

申请令牌的具体逻辑,通过密码的方式进行申请
http://localhost:40400/oauth/token?grant_type=password&username=xxxx&password=xxxx&clientId=xxxxx

private AuthToken applyToken(String username, String password, String clientId, String clientSecret){
        // http://localhost:40400/oauth/token 申请令牌的地址
        ServiceInstance serviceInstance = loadBalancerClient.choose(XcServiceList.XC_SERVICE_UCENTER_AUTH);

        // http://127.0.0.1:40400
        URI uri = serviceInstance.getUri();

        String url = uri + "/oauth/token";

        LinkedMultiValueMap<String,String> headers = new LinkedMultiValueMap<>();
        // 请求头携带认证信息
        headers.add("Authorization",getBasicCode(clientId,clientSecret));

        LinkedMultiValueMap<String,String> body = new LinkedMultiValueMap<>();
        // 通过密码模式获取access_token
        body.add("grant_type","password");
        body.add("username",username);
        body.add("password",password);
        body.add("client_id",clientId);
        // public HttpEntity(T body, MultiValueMap<String, String> headers) {
        HttpEntity<MultiValueMap<String,String>> httpEntity = new HttpEntity<>(body,headers);

        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> entity = restTemplate.exchange(url, HttpMethod.POST, httpEntity, Map.class);
        Map<String,String> map = entity.getBody();
   
        // 校验返回的信息是否存在错误
        if (map == null || map.get("refresh_token") == null ||
                map.get("access_token") == null || map.get("jti") == null) {

            String error_description = map.get("error_description");
            if (error_description.startsWith("坏的")){
                ExceptionCast.cast(AuthCode.AUTH_CREDENTIAL_ERROR);
            } else if (error_description == null) {
                ExceptionCast.cast(AuthCode.AUTH_ACCOUNT_NOTEXISTS);
            }
            ExceptionCast.cast(AuthCode.AUTH_TOKEN_GET_FAIL);
        }

        AuthToken authToken = new AuthToken();
        // 对token进行设置
        authToken.setAccess_token(map.get("jti"));
        authToken.setJwt_token(map.get("access_token"));
        authToken.setRefresh_token(map.get("refresh_token"));
        return authToken;
    }

头信息认证添加64位basic编码形式为: Basic clientId:clientSecret

// 获取basic64位编码
    private String getBasicCode(String clientId,String clientSecret){
        String string = clientId+":"+clientSecret;
        // 进行64位编码
        byte[] encode = Base64Utils.encode(string.getBytes());
        return "Basic "+new String(encode);
    }

将生成的token保存到redis中

private boolean saveTokenToRedis(AuthToken authToken,long time){
        String json = JSON.toJSONString(authToken);
        // 将token保存在redis中并设置过期时间
        redisTemplate.opsForValue().set(authToken.getAccess_token(),json,time, TimeUnit.SECONDS);

        // 判断是否存成功
        Long expire = redisTemplate.getExpire(authToken.getAccess_token());
        // 说明redis中存在
        return expire > 0;
    }

5、用户退出操作

用户退出操作主要两步:清空cookie,清空redis

 @GetMapping("/userlogout")
  public ResponseResult logout() {
        // 从cookie中取出唯一标识,根据唯一标识进行删除
        String uid = getCookie();
        // 从redis中清除token
        authService.delToken(uid);
        // 从cookie中清除token
        delToken(uid);
        return new ResponseResult(CommonCode.SUCCESS);
    }
private String getCookie(){
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        Map<String, String> map = CookieUtil.readCookie(request, "");
        if (map != null && map.get("uid") != null) {
            return map.get("uid");
        }
        return null;
    }
 private void delToken(String uid) {
        HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
        // 将cookie的时间设置为0
        CookieUtil.addCookie(response,cookieDomain,"/","uid",uid,0,false);
    }

6、资源服务配置

  资源服务器:指通过网关调用的一些其他的微服务
  在微服务中引入pom依赖 ,在resources下添加公钥文件
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>

package cn.hegongda.xuecheng.manage_course.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.stream.Collectors;

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) // 进行权限校验
public class AuthenticationResourcesConfig extends ResourceServerConfigurerAdapter {

    private static final String PUBLIC_KEY = "publickey.txt";//公钥

    //定义JwtTokenStore,使用jwt令牌
    @Bean
    public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
        return new JwtTokenStore(jwtAccessTokenConverter);
    }
    //定义JJwtAccessTokenConverter,使用jwt令牌
    @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setVerifierKey(getPubKey());
        return converter;
    }


/*** 获取非对称加密公钥 Key * @return 公钥 Key */

    private String getPubKey() {
        Resource resource = new ClassPathResource(PUBLIC_KEY);
        try {
            InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream());
            BufferedReader br = new BufferedReader(inputStreamReader);
            return br.lines().collect(Collectors.joining("\n"));
        } catch (IOException ioe) {
            return null;
        }
    }//Http安全配

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                 // 保证swagger正常使用
                .antMatchers("/v2/api-docs",
                        "/swagger-resources/configuration/ui",
                        "/swagger-resources","/swagger-resources/configuration/security",
                        "/swagger-ui.html","/webjars/**")
                .permitAll()
                .anyRequest()
                .authenticated();
    }
}


7、网关配置

添加pom依赖

 <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-netflix-zuul -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
                <version>1.4.6.RELEASE</version>
            </dependency>
            <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-netflix-eureka-client -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
                <version>2.0.3.RELEASE</version>
            </dependency>

application.yml进行配置

zuul:
  routes:
    manager-course:
      path: /course/courseview/*
      serviceId: xc-service-manage-course
      strip-prefix: false  #是否去掉前缀 /course ,false 不去掉,转发路径为 /xc-service-manage-course/course                 去掉 /xc-service-manage-course/
      sensitive-headers: # 是否将cookie传到下游服务器,不写将会全部传到下游服务器
      ignored-headers:

在路由之前需要进行认证,自定义过滤器继承ZuulFilter

@Component
public class LoginFilter extends ZuulFilter {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     *   pre:可以在请求被路由之前调用
        route:在路由请求时候被调用
        post:在route和error过滤器之后被调用
        error:处理请求时发生错误时被调用
     * @return
     */
    @Override
    public String filterType() {
        return "pre";
    }

    /**
     * 通过int值来定义过滤器的执行顺序
     * @return
     */
    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    /**
     *  用于自定义逻辑
     * @return
     */
    @Override
    public Object run() {

        // 获取request
        RequestContext currentContext = RequestContext.getCurrentContext();
        HttpServletRequest request = currentContext.getRequest();
        HttpServletResponse response = currentContext.getResponse();

        if (StringUtils.isEmpty(getJwtFromHeader(request))){
            setErrorMessage(currentContext,response);
            return null;
        }

        String accessToken = getAccessToken();
        if (checkJwt(accessToken) < 0) {
            setErrorMessage(currentContext,response);
            return null;
        }

        return null;
    }

    // 从cookie中获取accessToken
    private String getAccessToken(){
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        Map<String, String> map = CookieUtil.readCookie(request, "uid");
        if (map != null && map.get("uid") != null) {
            return map.get("uid");
        }
        return null;
    }

    // 从cookie中取出accessToken区redis中进行校验
    private Long checkJwt(String accessToken) {
        if (StringUtils.isEmpty(accessToken)){
            return -2L;
        }
        Long expire = stringRedisTemplate.getExpire(accessToken);
        return expire;
    }

    private String getJwtFromHeader(HttpServletRequest request){
        //头部取出令牌
        String authorization = request.getHeader("Authorization");
        if (StringUtils.isEmpty(authorization)){
            return null;
        }
        if (!authorization.startsWith("Bearer ")) {
            return null;
        }
        String jwt = authorization.substring(7);
        if (StringUtils.isEmpty(jwt)) {
            return null;
        }
        return jwt;
    }

/**
 封装错误信息
 */
    private void setErrorMessage(RequestContext currentContext, HttpServletResponse response){
        // 拒绝访问
        currentContext.setSendZuulResponse(false);
        // 设置返回码
        currentContext.setResponseStatusCode(200);
        // 构建响应信息
        ResponseResult responseResult = new ResponseResult(CommonCode.UNAUTHENTICATED);
        currentContext.setResponseBody(JSON.toJSONString(responseResult));
        response.setContentType("application/json;charset=utf-8");
    }
}

主要步骤:
* 从头中取出携带的令牌,判断令牌是否为空,为空则返回错误信息
* 从cookie中取出jwt的唯一标识,判断是否为空,为空,则返回错误信息
* 根据唯一标识在redis中获取jwt,判断其是否过期,过期则返回错误信息,否则,通过

完 ! 参考资料—在线学成项目

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值