oauth2.0+security+jwt网关登录认证

最近在研究springcloud中的网关认证,花费了一周时间,总算搭建出一个可以使用的简易框架,在此,将研究的步骤以及所踩的坑列出来。
本次搭建的结构大致如下:

如上图,步骤描述:
1、前端页面请求网关路由,网关路由判断是否包含access_token以及access_token是否已经过期。
2、不包含access_token或者已经失效,则返回403状态,前端根据这个状态码进行判断,自动调用网关登录接口。
3、用户登录以后,前端代码自动调用获取token的连接(http://localhost:8080/oauth/oauth/authorize?client_id=XXX&response_type=token&scope=1234&redirect_uri=/api/user/me)获取到access_token,然后将acceess_token设置进入cookie(此步骤由前端代码进行完成,网上的资料都只介绍了怎么获取access_token,却没有介绍怎么使用)。
4、前端代码将带有cookie为access_token的请求再次通过网关路由(apigateway)请求后端服务。
5、gateway通过access_token调用oauth2.0的check_token接口,进行校验,校验通过,oauth将所需要的用户信息返回,由gateway进行jwt加密并且放到请求头中,再由gateway带有此请求头调用主服务。
6、在主服务设置一过滤器,对所有的请求都进行jwt认证
首先判断有没有此请求头,没有直接返回401.
若有请求头,则对此请求头进行解密,解密成功则通过认证,解密失败,则返回401鉴权失败。
下面上干货:
oauth服务代码:
作为对用户鉴权的服务,网上已经有许多对它的介绍,大部分都附上了源码,大家可以参考下,我总结起来:最主要的就是对AuthorizationServerConfigurerAdapter类和WebSecurityConfigurerAdapter进行继承重写。
AuthorizationServerConfigurerAdapter是授权服务配置,它里面有三个方法,分别为
• ClientDetailsServiceConfigurer:这个configurer定义了客户端细节服务。客户详细信息可以被初始化,或者你可以参考现有的商店。
• AuthorizationServerSecurityConfigurer:在令牌端点上定义了安全约束。
• AuthorizationServerEndpointsConfigurer:定义了授权和令牌端点和令牌服务
在我的服务中,ClientDetailsServiceConfigurer我采用的是数据库存储客户端用户名和密码,然后进行配置方式。

@Bean
public TokenStore tokenStore() {
    return new JdbcTokenStore(dataSource);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    clients.jdbc(dataSource);
}
AuthorizationServerSecurityConfigurer我配置成如下配置:
public void configure(AuthorizationServerSecurityConfigurer security) {
    security.tokenKeyAccess("permitAll()")
            .checkTokenAccess("permitAll()")
            .allowFormAuthenticationForClients()
            .passwordEncoder(passwordEncoder());
}

其中passwordEncoder()表示密码需要加密,而这个加密我担心oauth本身的加密不太安全,所以又重写了一遍,在之前先给它进行一遍AES加密。代码如下:

@Bean
public PasswordEncoder passwordEncoder() {
    return new PasswordEncoder() {
//对密码进行加密
        @Override
        public String encode(CharSequence charSequence) {
            String param = AdvCryptUtil.encryStringBySHA256(String.valueOf(charSequence));
            return new BCryptPasswordEncoder().encode(param);
        }
//登录时校验密码(加密时已经对密码做过处理,则不能再使用oauth自带的比较)
        @Override
        public boolean matches(CharSequence rawPassword, String encodedPassword) {
            String param = AdvCryptUtil.encryStringBySHA256(String.valueOf(rawPassword));
            if (encodedPassword == null || encodedPassword.length() == 0) {
                return false;
            }
            if (!BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
                                   return  true;
                }
                return false;
            }
            return BCrypt.checkpw(param, encodedPassword);
        }
    };
}

最后一个AuthorizationServerEndpointsConfigurer的配置我设置如下:

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
    endpoints.authenticationManager(authenticationManager())
            .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
            .tokenStore(tokenStore())
            .accessTokenConverter(new CustomUserDetailConverter())
            .userDetailsService(userDetailsService());
}

这个配置里面需要注意的两个分别是.accessTokenConverter(new CustomUserDetailConverter())和.userDetailsService(userDetailsService());
这两个是自定义一些自己的方法,第一个是自定义需要获取到的用户信息(oauth自带的实体类并不能满足我们需求,所以需要自己定义一个。)第二个是定义我们去哪里获取用来和前端数据做校验的用户信息。
CustomUserDetailConverter

public class CustomUserDetailConverter extends DefaultAccessTokenConverter {
    private UserAuthenticationConverter userTokenConverter = new DefaultUserAuthenticationConverter();
    private boolean includeGrantType;
    public void setIncludeGrantType(boolean includeGrantType) {
        this.includeGrantType = includeGrantType;
    }
    /**
     * Converter for the part of the data in the token representing a user.
     *
     * @param userTokenConverter the userTokenConverter to set
     */
    public void setUserTokenConverter(UserAuthenticationConverter userTokenConverter) {
        this.userTokenConverter = userTokenConverter;
    }
    @Override
    public Map convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
        Map response = new HashMap();
        OAuth2Request clientToken = authentication.getOAuth2Request();
        if (!authentication.isClientOnly()) {
            response.putAll(userTokenConverter.convertUserAuthentication(authentication.getUserAuthentication()));
            SecurityUser userInfo = (SecurityUser)authentication.getUserAuthentication().getPrincipal();
            response.put("userId",userInfo.getUserId());
            response.put("nickName",userInfo.getNickName());
            response.put("roleId",userInfo.getRoleId());
        } else {
            if (clientToken.getAuthorities()!=null && !clientToken.getAuthorities().isEmpty()) {
                response.put(UserAuthenticationConverter.AUTHORITIES,
                        AuthorityUtils.authorityListToSet(clientToken.getAuthorities()));
            }
        }
        if (token.getScope()!=null) {
            response.put(SCOPE, token.getScope());
        }
        if (token.getAdditionalInformation().containsKey(JTI)) {
            response.put(JTI, token.getAdditionalInformation().get(JTI));
        }
        if (token.getExpiration() != null) {
            response.put(EXP, token.getExpiration().getTime() / 1000);
        }
        if (includeGrantType && authentication.getOAuth2Request().getGrantType()!=null) {
            response.put(GRANT_TYPE, authentication.getOAuth2Request().getGrantType());
        }
        response.putAll(token.getAdditionalInformation());
        response.put(CLIENT_ID, clientToken.getClientId());
        if (clientToken.getResourceIds() != null && !clientToken.getResourceIds().isEmpty()) {
            response.put(AUD, clientToken.getResourceIds());
        }
        return response;
    }
}

userDetailsService

public class CustomUserDetailsService implements UserDetailsService {
    @Autowired
    private UserService userService;
    @Override
    public UserDetails loadUserByUsername(String username) throws AccountNotExistsException {
        UserInfoDo user = userService.getUserByUsername(username);
        if (user == null) {
            throw new AccountNotExistsException("账号密码错误");
        }
        Collection authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
        SecurityUser securityUser = new SecurityUser();
        securityUser.setEmail(user.getUserName());
        securityUser.setPassword(user.getPassword());
        securityUser.setUserId(user.getUserId());
        securityUser.setNickName(user.getNickName());
        securityUser.setRoleId(user.getRoleId());
        return  securityUser;
    }

我这个是调用主服务去数据库获取用户名密码进行比较。
AuthorizationServerConfigurerAdapter的方法基本上已经完成
下面开始写WebSecurityConfigurerAdapter
这个类就简单的实现了一种功能,那就是拦截所有请求,并使用httpBasic方式登陆

@Override
protected void configure(HttpSecurity http) throws Exception {

    // 标准登录配置
    http
            .authorizeRequests()
            .antMatchers(
                     "/favicon.ico",
                    "/login",
                  "/authentication/**",
                    "/oauth/token",
                    "/actuator/**",
                    "/oauth/authorize"
            )
            .permitAll() // 允许匿名访问的地址
            .and() // 使用and()方法相当于XML标签的关闭,这样允许我们继续配置父类节点。
            .authorizeRequests()
            .anyRequest()
            .authenticated() // 其它地址都需进行认证
            .and()
            .formLogin() // 启用表单登录
            .loginPage(properties.getLoginPage()) // 登录页面
            //.defaultSuccessUrl("/") // 默认的登录成功后的跳转地址
            //.authenticationDetailsSource(authenticationDetailsSource)      //对比验证码
            .successHandler(authenticationSuccessHandler)
            .failureHandler(authenticationFailureHandler)
            .and()
            .logout()
            .logoutSuccessHandler(logoutSuccessHandler) // 退出成功处理器,不能和 logoutSuccessUrl 同时使用
            .logoutSuccessUrl("/") // 退出成功跳到首页
            .deleteCookies("JSESSIONID", "SESSION") // 删除 Cookie
            .and()
            .sessionManagement()
            .invalidSessionUrl(properties.getLoginPage()) // session 失效后跳转的页面
            //.maximumSessions(1) // 设置同一用户最多存在几个 Session,后登陆的用户将踢掉前面的用户
            //.maxSessionsPreventsLogin(true) // 当 Session 超出时,阻止后面的登录
            //.expiredSessionStrategy(new CustomSessionInformationExpiredStrategy()) // 用户失效处理
            //.and()
            .and()
            .csrf()
            .disable()
    ;
}

到这里,基本上oauth部分的代码已经写完了,下面是gateway和主服务代码。
Gateway部分:
初步规划是所有的请求都由gateway进行分发,所以,权限校验直接在gateway完成。
增加两个filter,第一个filter获取用户信息,代码如下:

@Override
@SuppressWarnings("unchecked")
public boolean run(RequestContext context) {
    String accessToken = context.request.accessToken;
    if (StringUtils.isEmpty(accessToken)) {
        //context.response.setStatus(CheckState.PERMISSION_ACCESS_TOKEN_NULL);
        context.response.setStatus(CheckState.SUCCESS_PASS_SITE);
        context.response.setMessage("Access_token is empty, Please login and set access_token by HTTP header 'Authorization'");
        return true;
    }
    CustomUserDetailsWithResult result = getUserDetailsService.getUserDetails(accessToken);
    context.response.setStatus(result.getState());
    context.response.setMessage(result.getMessage());
    if (result.getCustomUserDetails() == null) {
        return false;
    }
    context.setCustomUserDetails(result.getCustomUserDetails());
    return true;
}

第二个filter将用户信息进行JWT加密,然后加入请求头

@Override
public boolean run(RequestContext context) {
    try {
        if (context.getCustomUserDetails()==null){
            return  true;
        }
        String token = objectMapper.writeValueAsString(context.getCustomUserDetails());
        String jwt = "Bearer " + JwtHelper.encode(token, jwtSigner).getEncoded();
        context.response.setJwt(jwt);
        return true;
    } catch (JsonProcessingException e) {
        context.response.setStatus(CheckState.EXCEPTION_GATEWAY_HELPER);
        context.response.setMessage("gateway helper error happened: " + e.toString());
        return false;
    }
}

现在oauth和gateway的代码都完成了,就差最后一步,在主服务里面做校验。
主服务照样采用一个过滤器来进行,如果没有带请求头来调用主服务并且不是白名单的URL,则鉴权失败,代码如下:

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    HttpServletRequest httpRequest = (HttpServletRequest) request;
    String path=((HttpServletRequest) request).getRequestURI();
    try {
        Authentication authentication = this.tokenExtractor.extract(httpRequest);
        if (authentication == null) {
            if (isExclusion(ALLOWED_PATHS,path)){
                chain.doFilter(request, response);
                return;
            }
            if (this.isAuthenticated()) {
                LOGGER.debug("Clearing security context.");
                SecurityContextHolder.clearContext();
            }
            LOGGER.debug("No Jwt token in request, will continue chain.");
            ((HttpServletResponse) response).sendError(HttpServletResponse.SC_UNAUTHORIZED, "No Jwt token in request.");
            return;
        } else {
            request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
            if (authentication instanceof AbstractAuthenticationToken) {
                AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication;
                needsDetails.setDetails(new OAuth2AuthenticationDetails(httpRequest));
            }

            Authentication authResult = this.authenticate(authentication);
            LOGGER.debug("Authentication success: {}", authResult);
            SecurityContextHolder.getContext().setAuthentication(authResult);
            //检查是不是要求作者权限
            if (!checkIsAuthor(path,authResult)){
                LOGGER.debug("The user is not Author, will continue chain.");
                ((HttpServletResponse) response).sendError(HttpServletResponse.SC_PAYMENT_REQUIRED, "The user is not Author, will continue chain.");
                return;
            }
        }
        //校验权限信息
        chain.doFilter(request, response);
    } catch (OAuth2Exception e) {
        SecurityContextHolder.clearContext();
        LOGGER.debug("Authentication request failed: ", e);
        ((HttpServletResponse) response).sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid JWT token.");
    }
}

场景继续优化:
在实际的使用过程当中,发现总共需要分成两步来进行,第一步,通过页面进行授权,第二步,拿着授权code去获取token。
我只有一个主服务,直接进行的是密码授权,再经历一遍授权页面太麻烦了,所以我做了下优化,直接把client等信息写到oauth里面,等用户名密码校验通过,在成功的逻辑里面直接生成token进行返回:

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                    Authentication authentication) throws IOException, ServletException {
    String username = request.getParameter("username");
    username = StringUtils.defaultIfBlank(username, request.getParameter("mobile"));
    UserInfoDo user = userService.getUserByUsername(username);
    //userService.loginSuccess(user.getUserId());
    //Todo 登录成功后清除失败次数
    //response.sendRedirect("http://127.0.0.1:8085/oauth/api/user/me");
    //super.onAuthenticationSuccess(request, response, authentication);
    String clientId = "XXXXXX";
    String clientSecret = "12345";
    ClientDetails clientDetails =  clientDetailsService.loadClientByClientId(clientId);
    TokenRequest tokenRequest = new TokenRequest(null, clientId, clientDetails.getScope(), "custom");
    OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);
    OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);
    OAuth2AccessToken token = authorizationServerTokenServices.createAccessToken(oAuth2Authentication);
    Map map = new  HashMap();
    map.put("access_token",token.getValue());
    map.put("expires_in",token.getExpiresIn());
    map.put("nickName",user.getNickName());
    map.put("role",user.getRoleId());
    response.setContentType("application/json;charset=UTF-8");
    response.getWriter().write(objectMapper.writeValueAsString(map));
}

新手第一次写技术总结文档,写的有点简陋,仅提供给大家参考下,勿喷

你可能感兴趣的:(oauth2.0+security+jwt网关登录认证)