美文网首页
SpringBoot 实现 OAuth2 认证服务

SpringBoot 实现 OAuth2 认证服务

作者: xin_5457 | 来源:发表于2020-04-01 19:00 被阅读0次

    本文使用spring-security-oauth2实现OAuth2认证服务。
    源码地址:https://github.com/kangarooxin/spring-security-oauth2-demo

    1. pom.xml加入依赖
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.security.oauth</groupId>
                <artifactId>spring-security-oauth2</artifactId>
                <version>2.3.8.RELEASE</version>
            </dependency>
    
    1. 配置SpringSecurity
    @Configuration
    @EnableWebSecurity //启用SpringSecurity
    public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    
        /**
         * 配置Security处理授权服务器相关请求
         * 
         * @param httpSecurity
         * @throws Exception
         */
        @Override
        public void configure(HttpSecurity httpSecurity) throws Exception {
            httpSecurity.requestMatchers().antMatchers("/oauth/**", "/login", "/logout")//配置需要Security拦截的请求
                    .and().formLogin().permitAll().loginPage("/login")//指定登录页面
                    .and().logout().logoutUrl("/logout")//指定登出页面
                    .and().authorizeRequests().anyRequest().authenticated()//其它页面都需要鉴权认证
                    .and().csrf().disable()//不开启csrf
            ;
        }
    
        /**
         * 用户服务
         * 
         * 此处在内存创建了2个用户,实际场景替换为db服务查询,只需要实现UserDetailsService即可
         * 
         * @return
         */
        @Bean
        @Override
        public UserDetailsService userDetailsService() {
            InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
            manager.createUser(User.withUsername("user1").password(passwordEncoder.encode("123456")).authorities("USER").build());
            manager.createUser(User.withUsername("user2").password(passwordEncoder.encode("123456")).authorities("USER").build());
            return manager;
        }
    
        /**
         * 设置密码校验器
         * NoOpPasswordEncoder 直接文本比较  equals
         */
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    }
    
    1. 配置认证服务AuthorizationServer
    @Configuration
    @EnableAuthorizationServer //启用认证服务
    public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
    
        @Autowired
        private AuthenticationManager authenticationManager;
    
        @Autowired
        private UserDetailsService userDetailsService;
    
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        /**
         * *用来配置令牌端点(Token Endpoint)的安全约束。
         */
        @Override
        public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
            security.allowFormAuthenticationForClients()//默认/oauth/token认证是使用BasicAuth,此处设置允许通过表单URL提交client_id和client_secret。
                    .passwordEncoder(passwordEncoder)//设置密码编码器, 使用client_id登录时,密码加密要跟用户加密一致。
                    .checkTokenAccess("isAuthenticated()")//开启/oauth/check_token验证端口认证权限访问
            ;
        }
    
        /**
         * 配置OAuth2的客户端相关信息
         */
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.withClientDetails(oauth2ClientDetailsService());
        }
    
        /**
         * 配置授权服务器端点的属性
         *
         */
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints.authenticationManager(authenticationManager)
                    .tokenStore(oauth2TokenStore())//指定token存储服务
                    .authorizationCodeServices(authorizationCodeServices())//指定code生成服务
                    .userDetailsService(userDetailsService)//用户服务
                    .reuseRefreshTokens(false)//每次刷新token会创建新的refresh_token
            ;
        }
    
        /**
         * 设置令牌存储方式
         * InMemoryTokenStore 在内存中存储令牌。
         * RedisTokenStore 在Redis缓存中存储令牌。
         * JwkTokenStore 支持使用JSON Web Key (JWK)验证JSON Web令牌(JwT)的子Web签名(JWS)
         * JwtTokenStore 不是真正的存储,不持久化数据,身份和访问令牌可以相互转换。
         * JdbcTokenStore 在数据库存储,需要创建相应的表存储数据
         */
        @Bean
        public TokenStore oauth2TokenStore() {
            return new InMemoryTokenStore();
        }
    
        /**
         * 设置Code生成服务
         * 
         * @return
         */
        @Bean
        public AuthorizationCodeServices authorizationCodeServices() {
            return new InMemoryAuthorizationCodeServices();
        }
    
        /**
         * client服务
         * 
         * @return
         * @throws Exception
         */
        @Bean
        public ClientDetailsService oauth2ClientDetailsService() throws Exception {
            InMemoryClientDetailsServiceBuilder builder = new InMemoryClientDetailsServiceBuilder();
            builder.withClient("clientId")
                    .secret(passwordEncoder.encode("clientSecret"))
                    .scopes("scope1", "scope2")
                    .authorizedGrantTypes("client_credentials", "password", "authorization_code", "refresh_token")//支持的授权类型
                    .redirectUris("http://www.baidu.com") //回调url
                    .autoApprove(false) //允许自动授权
                    .accessTokenValiditySeconds((int)TimeUnit.HOURS.toSeconds(12))
                    .refreshTokenValiditySeconds((int)TimeUnit.DAYS.toSeconds(30))
            ;
            return builder.build();
        }
    }
    
    1. 配置资源服务ResourceServer
    @Configuration
    @EnableResourceServer //启用资源服务
    public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
    
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.requestMatchers().antMatchers("/api/**") //仅拦截资源服务相关请求
                    .and().authorizeRequests()
                    .antMatchers("/api/group1/**").access("#oauth2.hasScope('scope1')") //配置scope访问权限
                    .antMatchers("/api/group2/**").access("#oauth2.hasScope('scope2')")
                    .anyRequest().authenticated();
        }
    }
    
    1. 创建资源API
    @RestController
    @RequestMapping("/api/group1")
    public class Group1Controller {
    
        @RequestMapping
        public String hello() {
            return "hello group1";
        }
    }
    @RestController
    @RequestMapping("/api/group2")
    public class Group2Controller {
    
        @RequestMapping
        public String hello() {
            return "hello group2";
        }
    }
    
    1. 授权测试
    • 客户端凭证(client_credentials)

    一般用于资源服务器是应用的一个后端模块,客户端向认证服务器验证身份来获取令牌。

    curl -d "grant_type=client_credentials&client_id=clientId&client_secret=clientSecret" http://localhost:8080/oauth/token
    {
      "access_token" : "6c246e41-7d9a-4b69-b340-893635638ed8",
      "token_type" : "bearer",
      "expires_in" : 42444,
      "scope" : "scope1"
    }
    
    • 密码式(password)
      使用用户名/密码作为授权方式从授权服务器上获取令牌,一般不支持刷新令牌。
    curl -d "grant_type=password&username=username&password=password&client_id=clientId&client_secret=clientSecret" http://localhost:8080/oauth/token
    {
      "access_token" : "b7ca73bc-992c-4c23-abbd-f819aa220199",
      "token_type" : "bearer",
      "refresh_token" : "51661039-fb67-4e6b-a563-bd4c58b084c0",
      "expires_in" : 43199,
      "scope" : "scope1"
    }
    
    • 隐藏式(implicit)
      和授权码模式类似,只不过少了获取code的步骤,是直接获取令牌token的,适用于公开的浏览器单页应用,令牌直接从授权服务器返回,不支持刷新令牌,且没有code安全保证,令牌容易因为被拦截窃听而泄露。
      1). 访问:
      http://127.0.0.1:8080/oauth/authorize?client_id=clientId&response_type=token&scope=user&redirect_uri=http://www.baidu.com
      2). 登录成功,回调地址携带access_token:
      http://www.baidu.com/#access_token=551b968a-8e1f-4f4e-bc5e-e94544d982ec&token_type=bearer&expires_in=43199
    • 授权码(authorization_code)
      授权码模式(authorization_code)是功能最完整、流程最严密的授权模式,code保证了token的安全性,即使code被拦截,由于没有app_secret,也是无法通过code获得token的。
      1). 访问:
      http://127.0.0.1:8080/oauth/authorize?client_id=clientId&response_type=code&scope=scope1%20scope2&redirect_uri=http://www.baidu.com
      2). 登录成功,回调地址携带code:
      https://www.baidu.com/?code=s6htPB
      3). 使用code换取access_token
      curl -d "grant_type=authorization_code&code=s6htPB&redirect_uri=http://www.baidu.com&client_id=clientId&client_secret=clientSecret" http://127.0.0.1:8080/oauth/token
      {
        "access_token": "551b968a-8e1f-4f4e-bc5e-e94544d982ec",
        "token_type": "bearer", 
        "refresh_token": "248dff03-4124-4867-a3c9-7e299d2283c8", 
        "expires_in": 42759, 
        "scope":"scope1" 
      }  
      
      4). 刷新token
      curl -d "grant_type=refresh_token&refresh_token=248dff03-4124-4867-a3c9-7e299d2283c8&client_id=clientId&client_secret=clientSecret" http://127.0.0.1:8080/oauth/token
      {
         "access_token" : "39a2536a-d420-4798-abb3-1fc083cce887",
         "token_type" : "bearer",
         "refresh_token" : "7ba0dfcd-433e-4e5e-b06c-97c61bd54a9a",
         "expires_in" : 43199,
         "scope" : "scope1"
      }
      
    1. 访问测试
      http://127.0.0.1:8080/api/group1?access_token=39a2536a-d420-4798-abb3-1fc083cce887

    使用Redis存储AuthorizationCode

        @Bean
        public AuthorizationCodeServices authorizationCodeServices() {
            return new RandomValueAuthorizationCodeServices() {
    
                private static final String AUTH_CODE_KEY = "auth_code";
    
                @Override
                protected void store(String code, OAuth2Authentication authentication) {
                    RedisConnection conn = getConnection();
                    try {
                        conn.hSet(AUTH_CODE_KEY.getBytes(StandardCharsets.UTF_8), code.getBytes(StandardCharsets.UTF_8),
                                SerializationUtils.serialize(authentication));
                    } catch (Exception e) {
                        log.error("save authentication code " + code + " failed:", e);
                    } finally {
                        conn.close();
                    }
                }
    
                @Override
                protected OAuth2Authentication remove(String code) {
                    RedisConnection conn = getConnection();
                    try {
                        OAuth2Authentication authentication;
                        try {
                            byte[] bytes = conn.hGet(AUTH_CODE_KEY.getBytes(StandardCharsets.UTF_8), code.getBytes(StandardCharsets.UTF_8));
                            if(bytes == null) {
                                return null;
                            }
                            authentication = SerializationUtils.deserialize(bytes);
                        } catch (Exception e) {
                            return null;
                        }
    
                        if (null != authentication) {
                            conn.hDel(AUTH_CODE_KEY.getBytes(StandardCharsets.UTF_8), code.getBytes(StandardCharsets.UTF_8));
                        }
    
                        return authentication;
                    } catch (Exception e) {
                        log.error("remove authentication code " + code + "failed:", e);
                        return null;
                    } finally {
                        conn.close();
                    }
                }
    
                private RedisConnection getConnection() {
                    return redisConnectionFactory.getConnection();
                }
            };
        }
    

    自定义登录和授权页面

    1. pom.xml添加依赖
          <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-thymeleaf</artifactId>
            </dependency>
    
    1. 创建Oauth2Controller
    @Controller
    @RequestMapping("/oauth")
    public class Oauth2Controller {
    
        //登录页面
        @GetMapping("login")
        public String login() {
            return "oauth/login";
        }
        //授权页
        @GetMapping("confirm_access")
        public String authorizeGet() {
            return "oauth/confirm_access";
        }
        //授权错误页
        @GetMapping("error")
        public String error() {
            return "oauth/error";
        }
    }
    
    1. 在templates下创建页面模板
    • login.html
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>登录</title>
    </head>
    <body>
    <form method="post" action="/oauth/login">
      用户名:<input name="username" placeholder="请输入用户名" type="text">
      密码:<input name="password" placeholder="请输入密码" type="password">
      <input value="登录" type="submit">
    </form>
    </body>
    </html>
    
    • confirm_access.html
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>授权</title>
    </head>
    <body>
    <form action="/oauth/authorize" method="post">
      <input type="hidden" name="user_oauth_approval" value="true">
      <div id="scope"></div>
      <input type="submit" value="授权">
    </form>
    <script>
    function getQueryString(name) {
      var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");
      var r = window.location.search.substr(1).match(reg);
      if (r != null) return unescape(r[2]);
      return null;
    }
    </script>
    <script>
    var scope = getQueryString("scope");
    
    var scopeList = scope.split(" ");
    var html = "";
    for (var i = 0; i < scopeList.length; i++) {
      html += scopeList[i] + ":<input type='checkbox' name='scope." + scopeList[i] + "' value='true'/><br />";
    }
    document.getElementById("scope").innerHTML = html;
    </script>
    </body>
    </html>
    
    • error.html
    ${error}
    

    相关文章

      网友评论

          本文标题:SpringBoot 实现 OAuth2 认证服务

          本文链接:https://www.haomeiwen.com/subject/bwgauhtx.html