72961


Spring Boot、Spring Security を使って、  
Rest通信時にBASIC認証を実現したいと思っております。

Spring Boot：2.3.1  
Spring Security：2.3.1  
Java：8

以下のプログラムを使用して、  
BASIC認証用のID、パスワードを正しい値を使用すると、期待したデータを取得する事が出来ました。

[code]

    (期待したデータ)
    {"success":true,"data_size":1,"data_list":[{"tantousya_renban":null,"s_tantousya_renban":"Tc+06cbIxUuHTHytksKuAA\u003d\u003d","tantousya_id":"pJBprG+u7hMI5PRrB/laZw\u003d\u003d","simei":"9quCURq/vavcixX4nMziu6UujhxOcCToeE7YzH1VXNw\u003d"}]}
    
[/code]

[code]

    〇リクエストを受ける側の処理
    【WebSecurityBasicConfig】
    public class WebSecurityBasicConfig extends WebSecurityConfigurerAdapter  {
        
        @Autowired
        AccountUserDetailsService accountUserDetailsService;
        
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.antMatcher("/sample/json3/**");
            http.authorizeRequests().anyRequest().authenticated(); 
            http.httpBasic().realmName("Basic Authentication NG");
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
            http.csrf().disable();
            
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth
                .userDetailsService(accountUserDetailsService)
                .authenticationProvider(createAuthProvider());
        }
        
        public DaoAuthenticationProvider createAuthProvider() {
            DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
            authenticationProvider.setUserDetailsService(accountUserDetailsService);
            authenticationProvider.setPasswordEncoder(passwordEncoderBasic());
            return authenticationProvider;
        }
        
        @Bean
        public static PasswordEncoder passwordEncoderBasic() {
              return NoOpPasswordEncoder.getInstance();
        }
    }
    【AccountUserDetailsService】
    public class AccountUserDetailsService implements UserDetailsService {
        @Autowired
        private AccountMapper accountMapper;
        
        @Transactional(readOnly = true,isolation=Isolation.READ_COMMITTED,propagation=Propagation.REQUIRED)
        public UserDetails loadUserByUsername(String username)
        throws UsernameNotFoundException {
            Account account = accountMapper.findById(username);
            if(account == null) {
                throw new UsernameNotFoundException(username + " is not found");
            }
            return new AccountUserDetails(account,getAuthorities(account));
        }
        
        private Collection<GrantedAuthority> getAuthorities(Account account){
            if(account.isAdministrator()) {
                return AuthorityUtils.createAuthorityList("ROLE_USER","ROLE_ADMIN");
            }
            else {
                return AuthorityUtils.createAuthorityList("ROLE_USER","ROLE_ADMIN");
            }
        }
    }
    
    【SelectContainerLogic】
    @Controller
    @RequestMapping(value="/sample/json3") 
    public class SelectContainerLogic { 
    
        @Autowired
        TantousyaService tantousyaService;
        
        @RequestMapping(value="/testGetData",method=RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) 
        @ResponseBody 
        public String testGetData(@RequestParam String tantousya_id,@RequestParam String simei){ 
            
            JsonHolder holder = new JsonHolder();
            String secretKey = "Abcdefghijklmnop";
            String ivs = "17B1234567890123";
            String json_data = null;
            
            try {
                
                List<SelectContent> list = tantousyaService.getTantousya_list(tantousya_id);
                Crypto crypto = new Crypto(secretKey,ivs);
                
                if(list.size() != 0) {
                    holder.setSuccess(true);
                    holder.setData_size(list.size());
                    for(int i=0;i<list.size();i++) {
                        SelectContent data = (SelectContent)list.get(i);
                        data.setTantousya_id(crypto.encrypto(data.getTantousya_id()));
                        data.setSimei(crypto.encrypto(data.getSimei()));
                        data.setS_tantousya_renban(crypto.encrypto(data.getTantousya_renban().toString()));
                        data.setTantousya_renban(null);
                    }
                    holder.setData_list(list);
                }
                else {
                    holder.setSuccess(true);
                    holder.setData_size(0);
                }
                
                Gson gson = new GsonBuilder().serializeNulls().create(); 
                json_data = gson.toJson(holder);
                
            }
            catch(Exception e){
                e.printStackTrace();
            }
            return json_data;
        } 
        
    } 
    
    〇リクエストをする側の処理
    public class test_apri1 {
    
        public static void main(String[] args) {
           
        HttpURLConnection conn = null;
        StringBuffer json = new StringBuffer();  
        URL url = null;
        InputStream is = null;
        StringBuffer v_param = new StringBuffer();
        Map params = new HashMap();
        params.put("tantousya_id","KANRI");
        params.put("simei","kanri");
       
        try {  
           url = new URL("http://192.168.10.10:8080/sample/json3/testGetData");  
           conn = (HttpURLConnection)url.openConnection();  
           conn.setRequestMethod("POST");
           conn.setDoOutput(true);  
           conn.setUseCaches(false);
           conn.setRequestProperty("Accept-Language", "jp");    
           conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
           String v_id_pass ="TEST:TEST" ;
           byte[] v_id_pass_str64 = Base64.encodeBase64(v_id_pass.getBytes());
           conn.setRequestProperty("Authorization", "Basic "+ new String(v_id_pass_str64));
           conn.connect();  
           
           PrintStream ps = new PrintStream(conn.getOutputStream(),false,"MS932");  
           boolean v_syokai_flg = true;
           if(params != null){
               Iterator iterator = params.entrySet().iterator();
               while (iterator.hasNext()) {
                 Map.Entry entry = (Map.Entry)iterator.next();
                 if(v_syokai_flg){
                  v_syokai_flg = false;
                 }
                 else{
                  v_param.append("&");
                 }
                 v_param.append(entry.getKey().toString() + "=" + entry.getValue());
               }
           }
    
           ps.print(v_param.toString());  
           ps.flush();  
           ps.close();
           
           is = conn.getInputStream();  
           BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream(),"UTF-8"));
       
           String s;
           while ((s = reader.readLine()) != null) {  
              json.append(s);  
           }  
       
           System.out.println(json.toString());
       
        } catch(SocketTimeoutException e) {  
           e.printStackTrace();  
        } catch(Exception e) {  
           e.printStackTrace();  
        } finally {  
           if(conn != null){
           conn.disconnect();  
           }
        }
        }
    }
    
    
[/code]

ただ、BASIC認証用のID、パスワードに誤りの値を使用すると、以下の様なデータを取得します。

[code]

    (データ)
    <!DOCTYPE html><html lang="en">  <head>    <meta charset="utf-8">    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">    <meta name="description" content="">    <meta name="author" content="">    <title>Please sign in</title>    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">    <link href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css" rel="stylesheet" crossorigin="anonymous"/>  </head>  <body>     <div class="container">      <form class="form-signin" method="post" action="/login">        <h2 class="form-signin-heading">Please sign in</h2>        <p>          <label for="username" class="sr-only">Username</label>          <input type="text" id="username" name="username" class="form-control" placeholder="Username" required autofocus>        </p>        <p>          <label for="password" class="sr-only">Password</label>          <input type="password" id="password" name="password" class="form-control" placeholder="Password" required>        </p><input name="_csrf" type="hidden" value="04b3c2d7-1b82-42a1-863b-2e85e08b97fb" />        <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>      </form></div></body></html>
    
[/code]

BASIC認証用のID、パスワードに誤りの値を使用した際は、  
以下の様なJSONで認証NGだったメッセージや

[code]

    {"success":false,"data_size":0,"data_list":[]}
    
    
[/code]

以下の様な401等のエラーを発生させたいのですが、実施する方法はありますでしょうか。

[code]

    java.io.IOException: Server retuｊrned HTTP response code: 401 for URL: http://192.168.10.10:8080/sample/json3/testGetData
    
[/code]

よろしくお願いいたします。


CC BY-SA 4.0
2020-12-28T05:48:30.120
72939
2021-01-28T18:26:45.507
37340
question
spring-boot spring-security
Spring Boot、Spring SecurityによりRest通信時のBASIC認証について


まず、

> BASIC認証用のID、パスワードに誤りの値を使用すると、以下の様なデータを取得します。
[code]

>     (データ)
>     <!DOCTYPE html><html lang="en">  <head>    <meta charset="utf-8">
> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-
> to-fit=no">    <meta name="description" content="">    <meta name="author"
> content="">    <title>Please sign in</title>    <link
> href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css"
> rel="stylesheet"
> integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M"
> crossorigin="anonymous">    <link
> href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css"
> rel="stylesheet" crossorigin="anonymous"/>  </head>  <body>     <div
> class="container">      <form class="form-signin" method="post"
> action="/login">        <h2 class="form-signin-heading">Please sign in</h2>
> <p>          <label for="username" class="sr-only">Username</label>
> <input type="text" id="username" name="username" class="form-control"
> placeholder="Username" required autofocus>        </p>        <p>
> <label for="password" class="sr-only">Password</label>          <input
> type="password" id="password" name="password" class="form-control"
> placeholder="Password" required>        </p><input name="_csrf"
> type="hidden" value="04b3c2d7-1b82-42a1-863b-2e85e08b97fb" />        <button
> class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
> </form></div></body></html>
>  
[/code]

上のものは、[Form Login](https://docs.spring.io/spring-
security/site/docs/current/reference/html5/#servlet-authentication-
form)時のデフォルトの応答です。

質問文中の`WebSecurityBasicConfig`から察するに、[Basic認証](https://docs.spring.io/spring-
security/site/docs/current/reference/html5/#servlet-authentication-
basic)だけ有効化したいのだと思われますが、そうなっておらずSpring Bootのデフォルト設定のままになっている、ということになります。

質問文中の`WebSecurityBasicConfig`が想定通り有効化されているならばこのような応答にならず、クライアントの実行結果は期待している

[code]

    java.io.IOException: Server returned HTTP response code: 401 for URL: ...
    
[/code]

のような例外送出になるかと思います。

認証OKの場合、NGの場合双方とも質問文に書かれている結果になるような状況を、質問文のコードを穴埋めする形で考えてみましたが、思い浮かびませんでした。  
もしかするとコードの変更過程で結果が変わっているかも知れません。  
今の状態のコード、実行結果がそれぞれ質問文に記載しているものと同じか確認してみてください。

* * *

上の問題は置いておいて、`WebSecurityBasicConfig`が期待通り機能している状況で考えてみます。

認証失敗時のエラーハンドリング方法はいくつか有りますが、Spring Boot的に一番まっとうなやりかたは、`ErrorController`
を実装することかと考えます。

  * [4.7.1. The “Spring Web MVC Framework” > Error Handling](https://docs.spring.io/spring-boot/docs/2.3.1.RELEASE/reference/htmlsingle/#boot-features-error-handling)

デフォルト実装では[`BasicErrorController`](https://github.com/spring-projects/spring-
boot/blob/v2.3.1.RELEASE/spring-boot-project/spring-boot-
autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/BasicErrorController.java#L89-L107)というクラスが使われますが、これを挿げ替えて、所望のエラーレスポンスを返すようにします。  
例えば次のクラスを加えると、

[code]

    @Controller
    @RequestMapping("${server.error.path:${error.path:/error}}")
    public class MyErrorController extends AbstractErrorController {
    
        public MyErrorController(final ErrorAttributes errorAttributes, final List<ErrorViewResolver> errorViewResolvers) {
            super(errorAttributes, errorViewResolvers);
        }
    
        @Override
        public String getErrorPath() {
            return null;
        }
    
        @RequestMapping
        public ResponseEntity<Map<String, Object>> error(final HttpServletRequest request) {
            final HttpStatus status = getStatus(request);
    
            if (HttpStatus.UNAUTHORIZED.equals(status)) {
                final Map<String, Object> map = new HashMap<>();
                map.put("success", false);
                map.put("data_size", 0);
                map.put("data_list", Collections.emptyList());
                return new ResponseEntity<>(map, status);
            } else {
                return new ResponseEntity<>(status);
            }
        }
    }
    
[/code]

次のような結果が得られるようになります:

[code]

    $ curl --include -X POST http://192.168.10.10:8080/sample/json3/testGetData
    HTTP/1.1 401 
    WWW-Authenticate: Basic realm="Basic Authentication NG"
    X-Content-Type-Options: nosniff
    X-XSS-Protection: 1; mode=block
    Cache-Control: no-cache, no-store, max-age=0, must-revalidate
    Pragma: no-cache
    Expires: 0
    X-Frame-Options: DENY
    Content-Type: application/json
    Transfer-Encoding: chunked
    Date: Tue, 29 Dec 2020 08:18:01 GMT
    
    {"data_size":0,"data_list":[],"success":false}
    
[/code]

質問文中のクライアントコードで試す場合は、

[code]

    is = conn.getInputStream();
    BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream(),"UTF-8"));
    
[/code]

の部分を次のように書き換えます:

[code]

    // https://stackoverflow.com/a/34754502/4506703
    final BufferedReader reader;
    if (100 <= conn.getResponseCode() && conn.getResponseCode() <= 399) {
        reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
    } else {
        reader = new BufferedReader(new InputStreamReader(conn.getErrorStream()));
    }
    
[/code]

[サンプルコード](https://github.com/yukihane/stackoverflow-qa/tree/master/jaso72939)

* * *

追記

[こちらのやり方](https://ja.stackoverflow.com/a/73609/2808)は、Spring
Security的なやり方になります。  
文中で「Spring Boot的に一番まっとうなやりかた」と書いたのは、そのSpring Securityの機構も統合されているのが
`ErrorController` である、という意味です。  
間違ったやり方というわけではないので、実際にどちらでハンドルすべきかは、プロジェクトの方針などに拠ることになるでしょう。


CC BY-SA 4.0
2020-12-29T08:45:54.093
72961
2021-01-28T18:26:45.507
2021-01-28T18:26:45.507
2808
2808
72939
answer


ご連絡が遅くなりたいへん申し訳ございませんでした。  
ご連絡いただきました方法で、実現したいことが出来ました。  
ありがとうございました。

また、ご連絡いただきました方法以外に、  
以下の方法でも同じような事が実現できました。

以下の方法にした場合、何か問題はありますでしょうか。

[code]

    〇リクエストを受ける側の処理
    【WebSecurityBasicConfig】
    public class WebSecurityBasicConfig extends WebSecurityConfigurerAdapter  {
        
        @Autowired
        AccountUserDetailsService accountUserDetailsService;
        
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.antMatcher("/sample/json3/**");
            http.authorizeRequests().anyRequest().authenticated(); 
    　　　　// ココを変更しました。START
            http.httpBasic().realmName("Basic Authentication NG").authenticationEntryPoint(new CustomAuthenticationEntryPoint());  
    　　　　// ココを変更しました。END
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
            http.csrf().disable();
            
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth
                .userDetailsService(accountUserDetailsService)
                .authenticationProvider(createAuthProvider());
        }
        
        public DaoAuthenticationProvider createAuthProvider() {
            DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
            authenticationProvider.setUserDetailsService(accountUserDetailsService);
            authenticationProvider.setPasswordEncoder(passwordEncoderBasic());
            return authenticationProvider;
        }
        
        @Bean
        public static PasswordEncoder passwordEncoderBasic() {
              return NoOpPasswordEncoder.getInstance();
        }
    }
    
    // ココのクラスを追加しました。START
    【CustomAuthenticationEntryPoint】
    public class CustomAuthenticationEntryPoint extends BasicAuthenticationEntryPoint {
    
        @Override
        public void commence(HttpServletRequest request,
                             HttpServletResponse response,
                             AuthenticationException authException) throws IOException {
        
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
    
            PrintWriter writer = response.getWriter();
            JsonHolder holder = new JsonHolder();
            holder.setSuccess(false);
            holder.setData_size(0);
            holder.setData_list(Collections.emptyList());
            Gson gson = new GsonBuilder().serializeNulls().create(); 
            String json_data = gson.toJson(holder);
            writer.write(json_data);
        }
    
        @Override
        public void afterPropertiesSet() {
            setRealmName("Please Enter user and pass.");
            super.afterPropertiesSet();
        }
    }
    // ココのクラスを追加しました。END
    
    【AccountUserDetailsService】
    public class AccountUserDetailsService implements UserDetailsService {
        @Autowired
        private AccountMapper accountMapper;
        
        @Transactional(readOnly = true,isolation=Isolation.READ_COMMITTED,propagation=Propagation.REQUIRED)
        public UserDetails loadUserByUsername(String username)
        throws UsernameNotFoundException {
            Account account = accountMapper.findById(username);
            if(account == null) {
                throw new UsernameNotFoundException(username + " is not found");
            }
            return new AccountUserDetails(account,getAuthorities(account));
        }
        
        private Collection<GrantedAuthority> getAuthorities(Account account){
            if(account.isAdministrator()) {
                return AuthorityUtils.createAuthorityList("ROLE_USER","ROLE_ADMIN");
            }
            else {
                return AuthorityUtils.createAuthorityList("ROLE_USER","ROLE_ADMIN");
            }
        }
    }
    
    【SelectContainerLogic】
    @Controller
    @RequestMapping(value="/sample/json3") 
    public class SelectContainerLogic { 
    
        @Autowired
        TantousyaService tantousyaService;
        
        @RequestMapping(value="/testGetData",method=RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) 
        @ResponseBody 
        public String testGetData(@RequestParam String tantousya_id,@RequestParam String simei){ 
            
            JsonHolder holder = new JsonHolder();
            String secretKey = "Abcdefghijklmnop";
            String ivs = "17B1234567890123";
            String json_data = null;
            
            try {
                
                List<SelectContent> list = tantousyaService.getTantousya_list(tantousya_id);
                Crypto crypto = new Crypto(secretKey,ivs);
                
                if(list.size() != 0) {
                    holder.setSuccess(true);
                    holder.setData_size(list.size());
                    for(int i=0;i<list.size();i++) {
                        SelectContent data = (SelectContent)list.get(i);
                        data.setTantousya_id(crypto.encrypto(data.getTantousya_id()));
                        data.setSimei(crypto.encrypto(data.getSimei()));
                        data.setS_tantousya_renban(crypto.encrypto(data.getTantousya_renban().toString()));
                        data.setTantousya_renban(null);
                    }
                    holder.setData_list(list);
                }
                else {
                    holder.setSuccess(true);
                    holder.setData_size(0);
                }
                
                Gson gson = new GsonBuilder().serializeNulls().create(); 
                json_data = gson.toJson(holder);
                
            }
            catch(Exception e){
                e.printStackTrace();
            }
            return json_data;
        } 
        
    } 
    
    〇リクエストをする側の処理
    public class test_apri1 {
    
        public static void main(String[] args) {
           
        HttpURLConnection conn = null;
        StringBuffer json = new StringBuffer();  
        URL url = null;
        InputStream is = null;
        StringBuffer v_param = new StringBuffer();
        Map params = new HashMap();
        params.put("tantousya_id","KANRI");
        params.put("simei","kanri");
       
        try {  
           url = new URL("http://192.168.10.10:8080/sample/json3/testGetData");  
           conn = (HttpURLConnection)url.openConnection();  
           conn.setRequestMethod("POST");
           conn.setDoOutput(true);  
           conn.setUseCaches(false);
           conn.setRequestProperty("Accept-Language", "jp");    
           conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
           String v_id_pass ="TEST:TEST" ;
           byte[] v_id_pass_str64 = Base64.encodeBase64(v_id_pass.getBytes());
           conn.setRequestProperty("Authorization", "Basic "+ new String(v_id_pass_str64));
           conn.connect();  
           
           PrintStream ps = new PrintStream(conn.getOutputStream(),false,"MS932");  
           boolean v_syokai_flg = true;
           if(params != null){
               Iterator iterator = params.entrySet().iterator();
               while (iterator.hasNext()) {
                 Map.Entry entry = (Map.Entry)iterator.next();
                 if(v_syokai_flg){
                  v_syokai_flg = false;
                 }
                 else{
                  v_param.append("&");
                 }
                 v_param.append(entry.getKey().toString() + "=" + entry.getValue());
               }
           }
    
           ps.print(v_param.toString());  
           ps.flush();  
           ps.close();
           
    　　　  // ココを変更しました。 START
           final BufferedReader reader;
           if (100 <= conn.getResponseCode() && conn.getResponseCode() <= 399) {
                reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
            } else {
                reader = new BufferedReader(new InputStreamReader(conn.getErrorStream()));
            }   
    　　　  // ココを変更しました。 END
       
           String s;
           while ((s = reader.readLine()) != null) {  
              json.append(s);  
           }  
       
           System.out.println(json.toString());
       
        } catch(SocketTimeoutException e) {  
           e.printStackTrace();  
        } catch(Exception e) {  
           e.printStackTrace();  
        } finally {  
           if(conn != null){
           conn.disconnect();  
           }
        }
        }
    }
    
[/code]


CC BY-SA 4.0
2021-01-27T10:02:15.127
73609
2021-01-27T10:02:15.127
37340
72939
answer