当前位置:   article > 正文

springboot集成oauth2和JWT(授权码模式)_spring boot auth2 jwt

spring boot auth2 jwt

参考链接:

Oauth2.0简单解释
Oauth2.0四种方式
什么是JWT
JWT无状态登录
Spring security 系列15篇
Spring boot security 学习
Spring Security Oauth2 permitAll()还校验token
源码分析
UsernamePasswordAuthenticationFilter


前言

网上oauth2相关的demo讲的很笼统,几乎都是内存配置的方式简单演示了一下。

这段时间踩了很多坑,因此整理出了这篇文章

本文解决了如下问题:

  1. 前后端分离方式,进行oauth2配置,登录成功失败等全部处理,全部返回json
  2. 使用oauth的数据库方式配置
  3. 去除框架中的权限判断默认前缀 ROLE_
  4. 跨域问题,以及需要放行option请求
  5. jwt方式存储token,登出后token仍旧有效,采用登录成功时将token保存在redis中,登出或修改密码等操作删除redis中的token。并添加jwt过滤器,每次访问接口对token进行校验
  6. 资源服务器调用check_token接口,服务器直接500了,其实只是授权服务器发现token无效返回了400

未解决的问题:
1. 使用nginx做了负载均衡,搭建了两台资源服务器和两台认证服务器A B,登录资源服务器登录时调用了认证服务器A,获取授权码时(/oauth/authorize)调用了认证服务器B,返回401,尚不知道如何解决?


背景

客户使用多个系统,每个系统都有自己的账号密码,想通过我们的门户系统进行登录授权(其他系统登需要登录时,跳转门户进行登录授权)


服务器

前端服务器
资源服务器(子系统)
认证服务器

认证服务器和资源服务器分为两个项目(模块)


security oauth2框架的交互手段

前端与资源服务器交互(是否登录过):通过session交互
前端与资源服务器交互(每次都会调用认证服务器的/check_token):通过token(header中的Authorization属性)交互


认证流程

在这里插入图片描述


流程demo

  1. 前端服务器访问后端(资源服务器) 活动报名接口(/active)
  2. 资源服务器会调用认证服务器的/check_token接口,进行校验header中的token校验
  3. 校验不通过,返回401,前端跳转到登录页面。进行登录
  4. 登录成功,前端调用 http://127.0.0.1:8080/oauth/authorize?response_type=code&client_id=website&redirect_uri=http://192.168.10.182:8008/web-portal/su/token&scope=all 进行授权(自动授权)
  5. 认证成功,认证服务器回调: http://127.0.0.1:8008/web-portal/su/token?code=授权码
  6. 前端拿到返回的token信息,放到header中,与子系统进行交互


依赖问题

最初使用spring boot oauth2依赖有问题(据说不再维护)
因此采用spring-cloud-starter-oauth2的依赖

文章只提供oauth2 jwt相关的依赖

	<properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR9</spring-cloud.version>
    </properties>
    
    <dependencies>
      <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
            <version>1.0.9.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>18.0</version>
        </dependency>

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
    </dependencies>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28


数据库配置

表含义这里不多做介绍了,都是security oauth2框架使用数据库模式需要的表,认证配置的表是:oauth_client_details ,需要在该表中配置客户端id,回调URL等信息

CREATE TABLE oauth_access_token (
  token_id VARCHAR(256) NULL DEFAULT NULL,
  token TEXT NULL DEFAULT NULL,
  authentication_id VARCHAR(128) NOT NULL PRIMARY KEY,
  user_name VARCHAR(256) NULL DEFAULT NULL,
  client_id VARCHAR(256) NULL DEFAULT NULL,
  authentication text NULL DEFAULT NULL,
  refresh_token VARCHAR(256) NULL DEFAULT NULL);
ALTER TABLE public.oauth_access_token OWNER to dna_portal;

CREATE TABLE oauth_approvals (
  userId VARCHAR(256) NULL DEFAULT NULL,
  clientId VARCHAR(256) NULL DEFAULT NULL,
  scope VARCHAR(256) NULL DEFAULT NULL,
  status VARCHAR(10) NULL DEFAULT NULL,
  expiresAt time NULL DEFAULT NULL,
  lastModifiedAt time NULL DEFAULT NULL);
ALTER TABLE public.oauth_approvals OWNER to dna_portal;

CREATE TABLE oauth_client_details (
  client_id VARCHAR(128) NOT NULL PRIMARY KEY,
  resource_ids VARCHAR(256) NULL DEFAULT NULL,
  client_secret VARCHAR(256) NULL DEFAULT NULL,
  scope VARCHAR(256) NULL DEFAULT NULL,
  authorized_grant_types VARCHAR(256) NULL DEFAULT NULL,
  web_server_redirect_uri VARCHAR(256) NULL DEFAULT NULL,
  authorities VARCHAR(256) NULL DEFAULT NULL,
  access_token_validity INT8 NULL DEFAULT NULL,
  refresh_token_validity INT8 NULL DEFAULT NULL,
  additional_information VARCHAR(4096) NULL DEFAULT NULL,
  autoapprove VARCHAR(256) NULL DEFAULT NULL);
ALTER TABLE public.oauth_client_details OWNER to dna_portal;
insert into oauth_client_details (client_id,resource_ids,client_secret,scope,authorized_grant_types,
web_server_redirect_uri,authorities,access_token_validity,refresh_token_validity,additional_information,autoapprove) values
('website', 'website', '$2a$10$.ebjcgCVOHuEscJ6xLyQcu21nW93XuHZ2qk2TRbTofDLVhPY0C5S2', 'all', 'authorization_code,refresh_token',
'http://127.0.0.1:8080/web-portal/su/token', 'admin,ROLE_admin',36000, null, null, true);

CREATE TABLE oauth_client_token (
  token_id VARCHAR(256) NULL DEFAULT NULL,
  token text NULL DEFAULT NULL,
  authentication_id VARCHAR(128) NOT NULL PRIMARY KEY,
  user_name VARCHAR(256) NULL DEFAULT NULL,
  client_id VARCHAR(256) NULL DEFAULT NULL);
ALTER TABLE public.oauth_client_token OWNER to dna_portal;

CREATE TABLE oauth_code (
  code VARCHAR(256) NULL DEFAULT NULL,
  authentication text NULL DEFAULT NULL);
ALTER TABLE public.oauth_code OWNER to dna_portal;

CREATE TABLE oauth_refresh_token (
  token_id VARCHAR(256) NULL DEFAULT NULL,
  token text NULL DEFAULT NULL,
  authentication text NULL DEFAULT NULL);
ALTER TABLE public.oauth_refresh_token OWNER to dna_portal;

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56


资源服务器配置

跨域配置

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;


@Configuration
public class CorsConfig implements WebMvcConfigurer {

    /**
     * 跨域访问配置
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
                .allowCredentials(true)
                .maxAge(3600)
                .allowedHeaders("*");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

资源服务器配置

# 本应用占用端口
server:
  port: 8008
  servlet:
    context-path: /web-portal
    session:
      cookie:
        name: OAUTH2-PORTAL-SESSIONID

oauth2:
  server:
    # oauth_client_details配置的client_id
    clientId: website
    # oauth_client_details配置的client_secret(明文)
    clientSecret: 2020
    # oauth2-server服务的地址
    tokenAddr: http://127.0.0.1:8080/oauth2-server/oauth/token
    checkTokenAddr: http://127.0.0.1:8080/oauth2-server/oauth/check_token
    # jwt密钥
    jwtSecret: drinkless-jwt-key
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
import com.drinkless.portal.filter.JWTAuthenticationFilter;
import com.drinkless.portal.handlder.ApiAccessDeniedHandler;
import com.drinkless.portal.handlder.AuthExceptionEntryPoint;
import com.drinkless.portal.handlder.CustomerResponseErrorHandler;
import com.drinkless.portal.handlder.LoginExpireHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.config.http.SessionCreationPolicy;
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.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.web.client.RestTemplate;

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Value("${oauth2.server.checkTokenAddr}")
    private String checkTokenAddr;
    @Value("${oauth2.server.clientId}")
    private String clientId;
    @Value("${oauth2.server.clientSecret}")
    private String clientSecret;
    @Value("${oauth2.server.jwtSecret}")
    private String jwtSecret;

    //放行接口  swagger,系统连接性检查 和一些不需要登录就可访问的接口
    public static String[] passUrl =  {"/su/token/**",
                                "/region/getTree",
                                "/app/status",
                                "/sd/getDictList/**",
                                "/version",
                                "/article/getList",
                                "/article/find/**",
                                "/advertisement/list",
                                "/category/getCategoryList",
                                "/category/find/**",
                                "/advertisement/list",
                                "/_health",
                                "/v2/api-docs", "/swagger-resources/configuration/ui","/swagger-resources", "/swagger-resources/configuration/security",
                                "/swagger-ui.html","/css/**", "/js/**","/images/**", "/webjars/**", "**/favicon.ico"};

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(RestTemplateBuilder builder){
        RestTemplate restTemplate = builder.build();
        /*为RestTemplate配置异常处理器0*/
        restTemplate.setErrorHandler(new CustomerResponseErrorHandler());
        return restTemplate;
    }

    //不使用权限校验的ROLE_前缀  (http.servletApi().rolePrefix("");该方式无效果,使用@Bean方式)
    @Bean
    GrantedAuthorityDefaults grantedAuthorityDefaults() {
        return new GrantedAuthorityDefaults(""); // Remove the ROLE_ prefix
    }

    @Autowired
    private RestTemplate restTemplate;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId(clientId);

        resources.tokenStore(new JwtTokenStore(accessTokenConverter())).stateless(true);

        /* 配置令牌验证 */
        RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
        remoteTokenServices.setAccessTokenConverter(accessTokenConverter());
        remoteTokenServices.setRestTemplate(restTemplate);
        remoteTokenServices.setCheckTokenEndpointUrl(checkTokenAddr);
        remoteTokenServices.setClientId(clientId);
        remoteTokenServices.setClientSecret(clientSecret);

        resources.tokenServices(remoteTokenServices).stateless(true);
        //check_token异常类
        resources.authenticationEntryPoint(new AuthExceptionEntryPoint());
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(jwtSecret);
        return converter;
    }

    /* 配置资源拦截规则 */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        //关闭csrf
        http.cors().and().csrf().disable();

        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);

        //放行接口配置
        http.authorizeRequests()
                .antMatchers(HttpMethod.OPTIONS).permitAll() // 对option不校验
                .antMatchers(passUrl).permitAll()
                .anyRequest().authenticated();

        //jwt校验
        http.addFilterBefore(new JWTAuthenticationFilter(), AbstractPreAuthenticatedProcessingFilter.class);
        //登录超时未登录处理
        http.exceptionHandling().authenticationEntryPoint(new LoginExpireHandler());
        //权限不足处理器 配合注解使用@Secured("admin")
        http.exceptionHandling().accessDeniedHandler(new ApiAccessDeniedHandler());
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119

JWT过滤器

package com.drinkless.portal.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.drinkless.portal.common.constants.RedisConstant;
import com.drinkless.portal.common.entity.vo.SysUserVo;
import com.drinkless.portal.common.utils.ResultUtil;
import com.drinkless.portal.common.utils.SysUserUtil;
import com.drinkless.portal.config.ResourceServerConfig;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.context.support.WebApplicationContextUtils;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;

/**
 * jwt过滤器  配合redis校验
 */
@Slf4j
public class JWTAuthenticationFilter implements Filter {

    private final ObjectMapper mapper = new ObjectMapper();

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        log.info("【JWT过滤器】start");
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        //如果是放行接口,构造出无token的request后直接放行(这样做的话不会调用/oauth/check_token接口进行校验)
        String uri = request.getRequestURI();
        log.info("uri_{}:{}, request.getContentType(): {}", request.getMethod(), uri, request.getContentType());
        if (!isNeedFilter(uri, ResourceServerConfig.passUrl)) {
            request = new HttpServletRequestWrapper(request) {
                private Set<String> headerNameSet;

                @Override
                public Enumeration<String> getHeaderNames() {
                    if (headerNameSet == null) {
                        // first time this method is called, cache the wrapped request's header names:
                        headerNameSet = new HashSet<>();
                        Enumeration<String> wrappedHeaderNames = super.getHeaderNames();
                        while (wrappedHeaderNames.hasMoreElements()) {
                            String headerName = wrappedHeaderNames.nextElement();
                            if (!"Authorization".equalsIgnoreCase(headerName)) {
                                headerNameSet.add(headerName);
                            }
                        }
                    }
                    return Collections.enumeration(headerNameSet);
                }

                @Override
                public Enumeration<String> getHeaders(String name) {
                    if ("Authorization".equalsIgnoreCase(name)) {
                        return Collections.<String>emptyEnumeration();
                    }
                    return super.getHeaders(name);
                }

                @Override
                public String getHeader(String name) {
                    if ("Authorization".equalsIgnoreCase(name)) {
                        return null;
                    }
                    return super.getHeader(name);
                }
            };
            filterChain.doFilter(request, response);
        } else {
            //非放行的接口
            String header = request.getHeader(HttpHeaders.AUTHORIZATION);
            //是否过期
            boolean expireFlag = false;
            try {
                if (StringUtils.isNotBlank(header)) {
                    //token串
                    String token = header.substring(header.lastIndexOf("bearer") + 8);
                    // 获取ValueOperations bean
                    ServletContext context = request.getServletContext();
                    ApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(context);
                    ValueOperations valueOperations = (ValueOperations) ctx.getBean("valueOperations");
                    SysUserUtil sysUserUtil = (SysUserUtil) ctx.getBean("sysUserUtil");
                    SysUserVo user = sysUserUtil.getSysUserVoByToken(token);
                    String redisToken = (String) valueOperations.get(RedisConstant.REDIS_LOGIN + user.getUsername());
                    // token失效 或 token不正确
                    if (StringUtils.isBlank(redisToken) || !token.equals(redisToken)) {
                        expireFlag = true;
                    }
                } else {
                    // 没有token
                    expireFlag = true;
                }
            } catch (Exception e) {
                expireFlag = true;
                log.error("【jwtToken】校验出错,异常信息:{}", e);
            }
            if (expireFlag) {
                response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                response.setCharacterEncoding("UTF-8");
                response.setStatus(HttpStatus.UNAUTHORIZED.value());
                response.getWriter().write(this.mapper.writeValueAsString(ResultUtil.errorResult(
                        HttpStatus.UNAUTHORIZED.value(), "token失效")));
            } else {
                filterChain.doFilter(request, response);
            }
        }
    }





    /**
     * @param uri
     * @Description: 是否需要过滤
     */
    public boolean isNeedFilter(String uri, String[] includeUrls) {
        boolean isNeedFilter = true;
        for (String includeUrl : includeUrls) {
            includeUrl = includeUrl.replace("/**","");
            if (includeUrl.equals(uri) || uri.contains(includeUrl)) {
                isNeedFilter = false;
                break;
            }
        }
        return isNeedFilter;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141

权限不足处理器

package com.drinkless.portal.handlder;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.drinkless.portal.common.enums.HttpStatusEnum;
import com.drinkless.portal.common.result.Result;
import com.drinkless.portal.common.utils.ResultUtil;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * 403 权限不足处理器
 */
public class ApiAccessDeniedHandler implements AccessDeniedHandler {

    public final ObjectMapper MAPPER = new ObjectMapper();

    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        response.setStatus(HttpStatusEnum.FORBIDDEN.getCode());
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        Result result = ResultUtil.errorResult(HttpStatusEnum.FORBIDDEN.getCode(),
                "不允许访问");
        PrintWriter out = response.getWriter();
        out.write(MAPPER.writeValueAsString(result));
        out.flush();
        out.close();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

check_token异常类

package com.drinkless.portal.handlder;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.drinkless.portal.common.enums.HttpStatusEnum;
import com.drinkless.portal.common.result.Result;
import com.drinkless.portal.common.utils.ResultUtil;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * check_token异常类
 */
@Component
public class AuthExceptionEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException authException) throws ServletException {
        Map<String, Object> map = new HashMap<String, Object>();
        Throwable cause = authException.getCause();

        response.setStatus(HttpStatusEnum.UNAUTHORIZED.getCode());
        response.setHeader("Content-Type", "application/json;charset=UTF-8");
        try {
            if (cause instanceof InvalidTokenException) {
                Result result = ResultUtil.errorResult(HttpStatusEnum.UNAUTHORIZED.getCode(),
                        "token失效");
                response.getWriter().write(new ObjectMapper().writeValueAsString(result));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

restTemplate异常处理

package com.drinkless.portal.handlder;

import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.ResponseErrorHandler;

import java.io.IOException;

/**
 * restTemplate调用check_toke接口,token错误或失效,回返回400,抛出异常
 * 我们不想要抛异常,想让我们自定义的AuthExceptionEntryPoint去处理
 * 所以重写了hasError方法
 */
public class CustomerResponseErrorHandler implements ResponseErrorHandler {

    @Override
    public boolean hasError(ClientHttpResponse response) throws IOException {
        // 这里返回false
        return false;
    }

    @Override
    public void handleError(ClientHttpResponse response) throws IOException {

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

登录超时 未登录处理器

package com.drinkless.portal.handlder;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.drinkless.portal.common.enums.HttpStatusEnum;
import com.drinkless.portal.common.result.Result;
import com.drinkless.portal.common.utils.ResultUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @Description: 登录超时 未登录处理器
 * @Date: 2020/12/09 13:41
 */
@Slf4j
public class LoginExpireHandler implements AuthenticationEntryPoint {

    public final ObjectMapper MAPPER = new ObjectMapper();

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        response.setStatus(HttpStatusEnum.UNAUTHORIZED.getCode());
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        Result result = ResultUtil.errorResult(HttpStatusEnum.UNAUTHORIZED.getCode(),
                "登录过期或未登录");
        response.getWriter().write(MAPPER.writeValueAsString(result));
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

controller

package com.drinkless.portal.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.drinkless.portal.common.constants.RedisConstant;
import com.drinkless.portal.common.entity.dto.TokenDTO;
import com.drinkless.portal.common.entity.dto.UpdatePasswordDTO;
import com.drinkless.portal.common.entity.vo.SysUserVo;
import com.drinkless.portal.common.enums.ExceptionEnum;
import com.drinkless.portal.common.enums.ResultEnum;
import com.drinkless.portal.common.result.Result;
import com.drinkless.portal.common.service.SysUserService;
import com.drinkless.portal.common.utils.ResultUtil;
import com.drinkless.portal.common.utils.SysUserUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.http.*;
import org.springframework.security.access.annotation.Secured;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;

import javax.validation.Valid;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;

/**
 * @Description: 用户接口
 * @Date: 2019/11/27 14:44
 */
@Slf4j
@RestController
@RequestMapping("su")
@Api(description = "系统用户相关接口")
public class SysUserController {
    @Autowired
    private ValueOperations valueOperations;
    @Autowired
    private RestTemplate restTemplate;
    @Autowired
    private SysUserService sysUserService;
    @Autowired
    private SysUserUtil sysUserUtil;
    @Value("${oauth2.server.tokenAddr}")
    private String tokenAddr;
    @Value("${oauth2.server.clientId}")
    private String clientId;
    @Value("${oauth2.server.clientSecret}")
    private String clientSecret;

    // mapper 声明
    private final ObjectMapper mapper = new ObjectMapper();

    @GetMapping("token")
    @ApiOperation("获取token")
    public Result submitLogin(String code) throws Exception {
        RequestEntity httpEntity = new RequestEntity<>(getHttpBody(code), getHttpHeaders(),
                HttpMethod.POST, URI.create(tokenAddr));
        ResponseEntity<TokenDTO> exchange = restTemplate.exchange(httpEntity, TokenDTO.class);
        if (exchange.getStatusCode().is2xxSuccessful()) {
            // redis管理jwtToken失效
            TokenDTO tokenDTO = exchange.getBody();
            String accessToken = tokenDTO.getAccessToken();
            SysUserVo user = sysUserUtil.getSysUserVoByToken(accessToken);
            String expiresIn = tokenDTO.getExpiresIn();
            valueOperations.set(RedisConstant.REDIS_LOGIN + user.getUsername(), accessToken, Long.parseLong(expiresIn), TimeUnit.SECONDS);
            return ResultUtil.successResult(ResultEnum.SUCCESS_STATUS, exchange.getBody());
        }
        throw new RuntimeException("请求令牌失败!");
    }

    private MultiValueMap<String, String> getHttpBody(String code) throws UnsupportedEncodingException {
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("code", code);
        params.add("grant_type", "authorization_code");
        String redirectUri = sysUserService.selectOauthRedirectUri(clientId);
        params.add("redirect_uri", redirectUri);
        params.add("scope", "all");
        return params;
    }

    private HttpHeaders getHttpHeaders() {
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setBasicAuth(clientId, clientSecret);
        httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        httpHeaders.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
        return httpHeaders;
    }

 	//密码重置功能只允许管理员调用
 	//sprngboot启动类还需要添加以下配置,才可生效
 	//@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
    @GetMapping("password/reset/{id}")
    @ApiOperation("密码重置")
    @Secured("admin")
    public Result resetPassword(@PathVariable("id") Long id) {
        try {
            sysUserService.resetPassword(id);
            return ResultUtil.successResult(ResultEnum.SUCCESS_STATUS);
        } catch (Exception e) {
            log.error("【用户管理】密码重置异常, 异常信息:{}", e);
            return ResultUtil.errorResult(ExceptionEnum.UNKNOWN_EXCEPTION);
        }
    }

    @GetMapping("userInfo")
    @ApiOperation("获取用户信息接口")
    public Result<SysUserVo> queryUserInfo() {
        try {
        	SysUserVo vo = null;
            String header = request.getHeader(HttpHeaders.AUTHORIZATION);
	        if (StringUtils.isNotBlank(header)) {
	            //token串
	            String token = header.substring(header.lastIndexOf("bearer") + 8);
	            String tokenBody = JwtUtils.testJwt(token);
		        //token串转对象
		        JSONObject user = JSON.parseObject(tokenBody).getJSONObject("user");
		        vo = JSON.toJavaObject(user, SysUserVo.class);
	        }
            return ResultUtil.successResult(ResultEnum.SUCCESS_STATUS, vo);
        } catch (Exception e) {
            log.error("【用户管理】查询用户信息异常, 异常信息:{}", e);
            return ResultUtil.errorResult(ExceptionEnum.UNKNOWN_EXCEPTION);
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133

token对象

/**
 * 授权成功token返回体
 */
@Data
public class TokenDTO {
    @JsonProperty("access_token")
    private String accessToken;
    @JsonProperty("refresh_token")
    private String refreshToken;
    @JsonProperty("token_type")
    private String tokenType;
    @JsonProperty("expires_in")
    private String expiresIn;
    @JsonProperty("scope")
    private String scope;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

jwt工具类

package com.drinkless.oauth2.server.utils;

import org.springframework.stereotype.Component;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

@Component
public class JwtUtils {

    /* 默认head */
    public static final String DEFAULT_HEADER = "{\"alg\": \"HS256\",\"typ\": \"JWT\"}";

    /* token有效时间  1天 */
    public static final long EXPIRE_TIME = 1000*60*60*24;

    /* token在header中的名字 */
    public static final String HEADER_TOKEN_NAME = "Authorization";

    /** Base64URL 编码 */
    public static String encode(String input) {
        return Base64.getUrlEncoder().encodeToString(input.getBytes());
    }

    /** Base64URL 解码 */
    public static String decode(String input) {
        return new String(Base64.getUrlDecoder().decode(input));
    }




    /**
     * HmacSHA256 加密算法
     * @param data 要加密的数据
     * @param secret 秘钥
     */
    public static String HMACSHA256(String data, String secret) throws Exception {
        Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
        SecretKeySpec secret_key = new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256");
        sha256_HMAC.init(secret_key);
        byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8"));
        StringBuilder sb = new StringBuilder();
        for (byte item : array) {
            sb.append(Integer.toHexString((item & 0xFF) | 0x100), 1, 3);
        }
        return sb.toString().toUpperCase();
    }


    /** 获取签名 */
    public static String getSignature(String payload, String secret) throws Exception {
        return HMACSHA256(encode(DEFAULT_HEADER)+"."+encode(payload),secret);
    }


    /**
     * 验证jwt,正确返回载体数据,错误返回null
     * @param jwt
     */
    public static String testJwt(String jwt) throws Exception {
        String[] jwts = jwt.split("\\.");
        return decode(jwts[1]);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66


资源服务器的配置到这里结束


认证服务器配置

server:
  port: 8068
  servlet:
    context-path: /oauth2-server
    
oauth2:
  server:
    # jwt密钥
    jwtSecret: drinkless-jwt-key
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

授权服务配置

package com.drinkless.oauth2.server.config;

import com.drinkless.oauth2.server.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
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.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.List;

/**
 * 授权服务配置
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    @Qualifier("jwtTokenStore")
    private TokenStore tokenStore;

    @Autowired
    private DataSource dataSource;

    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;

    @Autowired
    private JwtTokenEnhancer jwtTokenEnhancer;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * 配置一个客户端
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.jdbc(dataSource).passwordEncoder(passwordEncoder);
    }

    /** 配置token管理 */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        TokenEnhancerChain chain = new TokenEnhancerChain();
        List<TokenEnhancer> list = new ArrayList<>();
        list.add(jwtTokenEnhancer);
        list.add(jwtAccessTokenConverter);
        chain.setTokenEnhancers(list);

        //通过注入密码授权被打开AuthenticationManager
        endpoints.authenticationManager(authenticationManager)
                .tokenStore(tokenStore)
                //刷新令牌授权将包含对用户详细信息的检查,以确保该帐户仍然活动,因此需要配置userDetailsService
                .userDetailsService(userDetailsService)
                //配置令牌生成
                .accessTokenConverter(jwtAccessTokenConverter)
                .tokenEnhancer(chain);

//                //该字段设置设置refresh token是否重复使用,true:reuse;false:no reuse.
//                .reuseRefreshTokens(true)

    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.tokenKeyAccess("permitAll()") //允许所有人请求令牌
                .checkTokenAccess("isAuthenticated()") //已验证的客户端才能请求check_token端点
                .allowFormAuthenticationForClients();

        // 单点登录配置
//        security.tokenKeyAccess("isAuthenticated()");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92

安全框架配置类

package com.drinkless.oauth2.server.config;

import com.drinkless.oauth2.server.handlder.LoginExpireHandler;
import com.drinkless.oauth2.server.handlder.LoginFailureHandler;
import com.drinkless.oauth2.server.handlder.LoginSuccessHandler;
import com.drinkless.oauth2.server.handlder.LogoutSuccessHandler;
import com.drinkless.oauth2.server.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

import javax.sql.DataSource;

/**
 * @Description: 安全框架配置类
 * @Date: 2020/11/30 21:03
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private DataSource dataSource;
    @Autowired
    private UserDetailsServiceImpl userDetailsService;
    @Autowired
    @Qualifier("jwtTokenStore")
    private TokenStore tokenStore;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //关闭csrf
        http.cors().and().csrf().disable();
        // 授权配置
        /* 开启授权认证 */
        http.authorizeRequests()
                .antMatchers("/oauth/**").permitAll() //允许访问授权接口
                .anyRequest().authenticated();

        //登录页面
        http.formLogin().loginProcessingUrl("/login");
        // 登录成功或失败处理
        http.formLogin().successHandler(new LoginSuccessHandler())
                .failureHandler(new LoginFailureHandler());
        // 登出授权及处理
        http.logout().logoutUrl("/logout")
                .logoutSuccessHandler(new LogoutSuccessHandler(tokenStore)).permitAll();
        //登录超时 未登录
        http.exceptionHandling().authenticationEntryPoint(new LoginExpireHandler());
    }


    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);

        // 自动建表
        //jdbcTokenRepository.setCreateTableOnStartup(true);

        return jdbcTokenRepository;
    }

    /** 授权服务配置需要用到这个bean */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /** 加密算法 */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92

jwt token配置

package com.drinkless.oauth2.server.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.IOException;

/**
 * jwt token配置
 */
@Configuration
public class JwtTokenStoreConfig {
    @Value("${oauth2.server.jwtSecret}")
    public String jwtSecret;

    @Bean
    public TokenStore jwtTokenStore()throws IOException {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() throws IOException {
        JwtAccessTokenConverter accessTokenConverter = new
                JwtAccessTokenConverter();
        //配置JWT使用的秘钥
        accessTokenConverter.setSigningKey(jwtSecret);
        return accessTokenConverter;
    }

    @Bean
    public JwtTokenEnhancer jwtTokenEnhancer() {
        return new JwtTokenEnhancer();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

jwt token 定制

package com.drinkless.oauth2.server.config;

import com.drinkless.oauth2.server.entity.SysUser;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;

import java.util.HashMap;
import java.util.Map;

/**
 * jwt token 定制
 */
public class JwtTokenEnhancer implements TokenEnhancer {

    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
        SysUser sysUser = (SysUser) oAuth2Authentication.getPrincipal();
        Map<String, Object> map = new HashMap<>();
        sysUser.setPassword(null);
        map.put("user", sysUser);
        ((DefaultOAuth2AccessToken)oAuth2AccessToken).setAdditionalInformation(map);
        return oAuth2AccessToken;
    }

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

userDetailsService实现类(框架会根据返回用户信息中的密码校验和前端(request中的password)传的是否一致)

package com.drinkless.oauth2.server.service;

import com.drinkless.oauth2.server.entity.SysUser;
import com.drinkless.oauth2.server.exception.MyUsernameNotFoundException;
import com.drinkless.oauth2.server.mapper.SysUserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;

/**
 * @Description: userDetailsService实现类
 * @Date: 2020/11/30 17:12
 */
@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private SysUserMapper sysUserMapper;
    @Autowired
    private HttpServletRequest request;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser sysUser = sysUserMapper.selectByUsername(username);
        if (sysUser == null) {
            throw new MyUsernameNotFoundException(username + " is not exists!");
        }
        if (!BCrypt.checkpw(request.getParameter("password"), sysUser.getPassword())) {
            throw new MyUsernameNotFoundException(username + " password error!");
        }

        return sysUser;
    }
}

@Data
public class SysUser implements UserDetails {
    /* 用户id */
    private Long id;

    /* 用户名 */
    private String username;
    /* 密码 */
    private String password;

    private String nickname;

    private String province;

    private String serverNos;

    private String idCardNo;

    private Date birthDate;

    private String gender;

    private String phone;

    private Boolean status;

    private Boolean deleteFlag;

    private Date createDatetime;

    private Date updateDatetime;

    /* 角色列表 */
    private List<SysRole> authorities = new ArrayList<>();

    /* 指示是否未过期的用户的凭据(密码),过期的凭据防止认证 默认true 默认表示未过期 */
    private boolean credentialsNonExpired = true;

    //账户是否未过期,过期无法验证 默认true表示未过期
    private boolean accountNonExpired = true;

    //用户是未被锁定,锁定的用户无法进行身份验证 默认true表示未锁定
    private boolean accountNonLocked = true;

    //是否可用 ,禁用的用户不能身份验证 默认true表示可用
    private boolean enabled = true;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88

登录成功处理器

package com.drinkless.oauth2.server.handlder;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.drinkless.oauth2.server.entity.SysRole;
import com.drinkless.oauth2.server.entity.SysUser;
import com.drinkless.oauth2.server.enums.ResultEnum;
import com.drinkless.oauth2.server.utils.ResultUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

/**
 * @Description: 登录成功处理
 * @Date: 2020/11/30 21:48
 */
@Slf4j
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

    public final ObjectMapper MAPPER = new ObjectMapper();

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        SysUser user = (SysUser) authentication.getPrincipal();
        log.info("【登录】用户名:{}登录成功", user.getUsername());
        log.info("【登录】权限:{}", user.getAuthorities());
        // 返回权限
        HashMap<Object, Object> map = new HashMap<>();
        List<SysRole> authorities = user.getAuthorities();
        List<String> roles = new ArrayList<>();
        for (SysRole role : authorities) {
            roles.add(role.getAuthority());
        }
        map.put("roles", roles);
        // 返回给前端
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        response.getWriter().write(MAPPER.writeValueAsString(ResultUtil.successResult(ResultEnum.SUCCESS_STATUS, map)));
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

登录失败处理器

package com.drinkless.oauth2.server.handlder;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.drinkless.oauth2.server.entity.Result;
import com.drinkless.oauth2.server.enums.ExceptionEnum;
import com.drinkless.oauth2.server.utils.ResultUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 登录失败处理器
 */
@Slf4j
public class LoginFailureHandler implements AuthenticationFailureHandler {

    public final ObjectMapper MAPPER = new ObjectMapper();

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        Result result = ResultUtil.errorResult(ExceptionEnum.UNKNOWN_EXCEPTION.getCode(),
                "登录失败");
        String username = request.getParameter("username");
        if (e.getMessage().contains("is not exists") || e.getMessage().contains("password error")) {
            result = ResultUtil.errorResult(ExceptionEnum.UNKNOWN_EXCEPTION.getCode(),
                    "用户或密码错误");
        }
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        response.getWriter().write(MAPPER.writeValueAsString(result));
        log.error("【登录失败】用户名:{}", username);
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

登录超时 未登录处理器

package com.drinkless.oauth2.server.handlder;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.drinkless.oauth2.server.entity.Result;
import com.drinkless.oauth2.server.enums.HttpStatusEnum;
import com.drinkless.oauth2.server.utils.ResultUtil;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @Description: 登录超时 未登录处理器
 * @Date: 2020/12/09 13:41
 */
public class LoginExpireHandler implements AuthenticationEntryPoint {

    public final ObjectMapper MAPPER = new ObjectMapper();

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        response.setStatus(HttpStatusEnum.UNAUTHORIZED.getCode());
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        Result result = ResultUtil.errorResult(HttpStatusEnum.UNAUTHORIZED.getCode(),
                "登录过期或未登录");
        response.getWriter().write(MAPPER.writeValueAsString(result));
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

登出成功处理

package com.drinkless.oauth2.server.handlder;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.drinkless.oauth2.server.constants.RedisConstant;
import com.drinkless.oauth2.server.entity.Result;
import com.drinkless.oauth2.server.enums.ResultEnum;
import com.drinkless.oauth2.server.utils.JwtUtils;
import com.drinkless.oauth2.server.utils.ResultUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.web.context.support.WebApplicationContextUtils;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.concurrent.TimeUnit;

/**
 * @Description: 登出成功处理
 * @Date: 2020/12/14 13:31
 */
@Slf4j
public class LogoutSuccessHandler implements org.springframework.security.web.authentication.logout.LogoutSuccessHandler {

    public final ObjectMapper MAPPER = new ObjectMapper();

    private TokenStore tokenStore;

    public LogoutSuccessHandler(TokenStore tokenStore) {
        this.tokenStore = tokenStore;
    }

    @Override
    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        try {
            // 获取用户信息
            String header = httpServletRequest.getHeader(HttpHeaders.AUTHORIZATION);
            String token = header.substring(header.lastIndexOf("bearer") + 8);
            String tokenBody = JwtUtils.testJwt(token);
            JSONObject user = JSON.parseObject(tokenBody).getJSONObject("user");
            // 清除redis中的token
            ServletContext context = httpServletRequest.getServletContext();
            ApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(context);
            ValueOperations valueOperations = (ValueOperations) ctx.getBean("valueOperations");
            valueOperations.set(RedisConstant.REDIS_LOGIN
                    + user.get("username"), "", 1, TimeUnit.MICROSECONDS);
            //返回退出成功信息
            httpServletResponse.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
            Result result = ResultUtil.successResult(ResultEnum.SUCCESS_STATUS);
            PrintWriter out = httpServletResponse.getWriter();
            out.write(MAPPER.writeValueAsString(result));
            out.flush();
            out.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69

找不到用户异常类

package com.drinkless.oauth2.server.exception;

import org.springframework.security.core.AuthenticationException;

/**
 * @Description: 找不到用户异常
 * @Date: 2020/12/10 14:48
 */
public class MyUsernameNotFoundException extends AuthenticationException {

    public MyUsernameNotFoundException(String msg) {
        super(msg);
    }

    public MyUsernameNotFoundException(String msg, Throwable t) {
        super(msg, t);
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

跨域

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    /**
     * 跨域访问配置
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
                .allowCredentials(true)
                .maxAge(3600)
                .allowedHeaders("*");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小蓝xlanll/article/detail/84445
推荐阅读
相关标签
  

闽ICP备14008679号