spring boot:spring security實現oauth2+jwt管理認證授權及oauth2返回結果格式化(spring boot 2.3.3)


一,為什么oauth2要整合jwt?

1,OAuth2的token技術有一個最大的問題是不攜帶用戶信息,所以資源服務器不能進行本地驗證,

  以致每次對於資源的訪問,資源服務器都需要向認證服務器的token存儲發起請求,

  一是驗證token的有效性,二是獲取token對應的用戶信息。

  有大量的請求時會導致處理效率降低,

  而且認證服務器作為一個中心節點,

   對於SLA和處理性能等均有很高的要求

   對於分布式架構都是可能引發問題的隱患

2,jwt技術的兩個優勢:

     token的簽名驗證可以直接在資源服務器本地完成,不需要再次連接認證服務器

     jwt的payload部分可以保存用戶相關信息,這樣直接有了token和用戶信息的綁定

 

3,spring security oauth2生成token時的輸出默認格式不能直接修改,

   演示例子中通過增加一個controller實現了輸出的格式化

 

說明:劉宏締的架構森林是一個專注架構的博客,地址:https://www.cnblogs.com/architectforest

         對應的源碼可以訪問這里獲取: https://github.com/liuhongdi/

說明:作者:劉宏締 郵箱: 371125307@qq.com

 

二,演示項目的相關信息

1,項目地址:

https://github.com/liuhongdi/securityoauth2jwt

 

2,項目功能說明:

        演示了使用jwt存儲oauth2的token

 

3,項目結構:如圖:

 

三,配置文件說明

1,pom.xml

       <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--security begin-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!--oauth2 begin-->
        <dependency>
<groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
            <version>2.5.0.RELEASE</version>
        </dependency>
        <!--jwt begin-->
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
            <version>1.1.1.RELEASE</version>
        </dependency>
        <!--mysql mybatis begin-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!--fastjson begin-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.73</version>
        </dependency>
        <!--jaxb-->
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.0</version>
        </dependency>
        <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-impl</artifactId>
            <version>2.3.0</version>
        </dependency>
        <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-core</artifactId>
            <version>2.3.0</version>
        </dependency>
        <dependency>
            <groupId>javax.activation</groupId>
            <artifactId>activation</artifactId>
            <version>1.1.1</version>
        </dependency>
        <!--jjwt begin-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <!--validation begin-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

 

2,application.properties

#mysql
spring.datasource.url=jdbc:mysql://localhost:3306/security?characterEncoding=utf8&useSSL=false
spring.datasource.username=root
spring.datasource.password=lhddemo
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

#mybatis
mybatis.mapper-locations=classpath:/mapper/*Mapper.xml
mybatis.type-aliases-package=com.example.demo.mapper

#error
server.error.include-stacktrace=always
#log
logging.level.org.springframework.web=trace

 

3,數據庫:

建表sql:

CREATE TABLE `sys_user` (
 `userId` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
 `userName` varchar(100) NOT NULL DEFAULT '' COMMENT '用戶名',
 `password` varchar(100) NOT NULL DEFAULT '' COMMENT '密碼',
 `nickName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '昵稱',
 PRIMARY KEY (`userId`),
 UNIQUE KEY `userName` (`userName`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用戶表'
INSERT INTO `sys_user` (`userId`, `userName`, `password`, `nickName`) VALUES
(1, 'lhd', '$2a$10$yGcOz3ekNI6Ya67tqQueS.raxyTOedGsv5jh2BwtRrI5/K9QEIPGq', '老劉'),
(2, 'admin', '$2a$10$yGcOz3ekNI6Ya67tqQueS.raxyTOedGsv5jh2BwtRrI5/K9QEIPGq', '管理員'),
(3, 'merchant', '$2a$10$yGcOz3ekNI6Ya67tqQueS.raxyTOedGsv5jh2BwtRrI5/K9QEIPGq', '商戶老張');

說明:3個密碼都是111111,僅供演示使用

CREATE TABLE `sys_user_role` (
 `urId` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
 `userId` int(11) NOT NULL DEFAULT '0' COMMENT '用戶id',
 `roleName` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '角色id',
 PRIMARY KEY (`urId`),
 UNIQUE KEY `userId` (`userId`,`roleName`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用戶角色關聯表'
INSERT INTO `sys_user_role` (`urId`, `userId`, `roleName`) VALUES
(1, 2, 'ADMIN'),
(2, 3, 'MERCHANT');

 

四,java代碼說明

1,WebSecurityConfig.java

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    private final static BCryptPasswordEncoder ENCODER = new BCryptPasswordEncoder();
    @Resource
    private SecUserDetailService secUserDetailService;     //用戶信息類,用來得到UserDetails

    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }
    @Bean
    @Override
    protected UserDetailsService userDetailsService() {
        return super.userDetailsService();
    }

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.antMatcher("/oauth/**")
                .authorizeRequests()
                .antMatchers("/oauth/**").permitAll()
                .and().csrf().disable();
    }

    @Resource
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(secUserDetailService).passwordEncoder(new PasswordEncoder() {
            @Override
            public String encode(CharSequence charSequence) {
                return ENCODER.encode(charSequence);
            }
            //密碼匹配,看輸入的密碼經過加密與數據庫中存放的是否一樣
            @Override
            public boolean matches(CharSequence charSequence, String s) {
                return ENCODER.matches(charSequence,s);
            }
        });
    }
}

 

2,AuthorizationServerConfig.java

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    @Resource
    AuthenticationManager authenticationManager;

    @Resource
    UserDetailsService userDetailsService;

    @Resource
    TokenStore jwtTokenStore;

    @Resource
    JwtAccessTokenConverter jwtAccessTokenConverter;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        String clientId = "client_id";
        String clientSecret = "123";
        clients.inMemory()
                //這個好比賬號
                .withClient(clientId)
                //授權同意的類型
                .authorizedGrantTypes("password", "refresh_token")
                //有效時間
                .accessTokenValiditySeconds(1800)
                .refreshTokenValiditySeconds(60 * 60 * 2)
                .resourceIds("rid")
                //作用域,范圍
                .scopes("all")
                //密碼
                .secret(new BCryptPasswordEncoder().encode(clientSecret));
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenStore(jwtTokenStore)
                .authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService)
                .accessTokenConverter(jwtAccessTokenConverter);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        //允許客戶端表單身份驗證
        security.allowFormAuthenticationForClients();
    }
}

授權服務器的配置

 

3,ResourceServerConfig.java

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    private static final String RESOURCE_ID = "rid";

    @Resource
    private RestAuthenticationEntryPoint restAuthenticationEntryPoint;

    @Resource
    private RestAccessDeniedHandler restAccessDeniedHandler;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId(RESOURCE_ID).stateless(false)
                .authenticationEntryPoint(restAuthenticationEntryPoint);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/**").hasAnyRole("admin","ADMIN");
        http.
                anonymous().disable()
                .authorizeRequests()
                .antMatchers("/users/**").access("hasRole('ADMIN')")
                .and().exceptionHandling().accessDeniedHandler(new OAuth2AccessDeniedHandler());

        //http.exceptionHandling().authenticationEntryPoint(restAuthenticationEntryPoint);
        http.exceptionHandling().accessDeniedHandler(restAccessDeniedHandler);
    }
}

資源服務器的配置

 

4,RestAccessDeniedHandler.java

@Component("restAccessDeniedHandler")
public class RestAccessDeniedHandler implements AccessDeniedHandler {
    //處理權限不足的情況:403,對應:access_denied
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       AccessDeniedException accessDeniedException)
            throws IOException, ServletException {
        System.out.println("-------RestAccessDeniedHandler");
        ServletUtil.printRestResult(RestResult.error(403,"權限不夠訪問當前資源,被拒絕"));
    }
}

遇到access deny情況的處理

 

5,RestAuthenticationEntryPoint.java

@Component("restAuthenticationEntryPoint")
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
    //返回未得到授權時的報錯:對應:invalid_token
    @Override
    public void commence(
            HttpServletRequest request,
            HttpServletResponse response,
            AuthenticationException authException) throws IOException {
        //System.out.println("commence");
        ServletUtil.printRestResult(RestResult.error(401,"未得到授權"));
    }
}

匿名用戶無權訪問時的處理

 

6,JwtTokenConfig.java

@Configuration
public class JwtTokenConfig {
    @Bean
    public TokenStore jwtTokenStore(){
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    //使用Jwt來作為token的生成
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
        accessTokenConverter.setSigningKey("internet_plus");
        return accessTokenConverter;
    }

    @Bean
    public TokenEnhancer jwtTokenEnhancer(){
        return new JwtTokenEnhancer ();
    }
}

配置jwttoken,指定了signkey

 

7,JwtTokenEnhancer.java

public class JwtTokenEnhancer implements TokenEnhancer {

    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        Map<String,Object> info = new HashMap<>();
        info.put("provider","haolarn");
        //設置附加信息
        ((DefaultOAuth2AccessToken)accessToken).setAdditionalInformation(info);
        return null;
    }
}

返回token信息中的附加信息

 

8,OauthController.java

@RestController
@RequestMapping("/oauth")
public class OauthController {

    @Autowired
    private TokenEndpoint tokenEndpoint;

   //自定義返回信息添加基本信息
    @PostMapping("/token")
    public RestResult postAccessTokenWithUserInfo(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
        OAuth2AccessToken accessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody();
        Map<String, Object> data = new LinkedHashMap();
        data.put("accessToken", accessToken.getValue());
        data.put("token_type", accessToken.getTokenType());
        data.put("refreshToken", accessToken.getRefreshToken().getValue());
        data.put("scope", accessToken.getScope());
        data.put("expires_in", accessToken.getExpiresIn());
        data.put("jti", accessToken.getAdditionalInformation().get("jti"));
        return RestResult.success(data);
    }

}

格式化生成token時的返回json信息

 

9,其他非關鍵代碼可以訪問github查看,不再一一貼出

 

五,測試效果

1,得到基於jwt的token

訪問:http://127.0.0.1:8080/oauth/token

 

 返回結果:

 

 

2,用得到的access_token訪問admin/hello:查看當前用戶和role:

http://127.0.0.1:8080/admin/hello

返回:

 

 

3,換一個無權限的賬號登錄:

 

訪問admin/hello時會提示無權限

 

 

 

六,查看spring boot的版本:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.3.3.RELEASE)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM