

### 実現したいこと

Spring Bootを使用し、予約サイトを作っています。  
現在Spring
Security用のDialectを使用し、sec:authorizeでRoleがADMINまたはログインユーザーIDと予約者のユーザーIDが一致した場合に予約取り消しフォームを表示する機能を実装中です。

### 発生している問題

ログインユーザーのRoleがADMINでも、予約取り消しフォームが表示されない。一方で、ログインユーザーIDと予約者のユーザーIDが一致した場合は予約取り消しフォームは表示されています。デバックしたところ、RoleにはきちんとADMINがセットされています。エラーメッセージなどは出ていません。

### 試したこと

色々と調べてみたところ自分と同じような問題をかかえている方がおり、その人はhasRoleではなくhasAuthorityを使用すると上手く出来たそうです。しかし、自分も試してみましたが上手くいきませんでした。試しにhasRoleにUSERを入れてみましたが、それも上手くいきませんでした。  
その後も調べていましたが、解決に至らずここで質問をさせて頂いています。よろしくお願いします。

* * *

### ソースコード

reserveForm.html

[code]

    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org"
          xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-spring-security5">
    <head>
    <meta charset="UTF-8">
    <title
        th:text="|${#temporals.format(date, 'yyyy/M/d')}の${room.roomName}|"></title>
    </head>
    <body th:with="user=${#authentication.principal.user}">
    
        <div>
            <a th:href="@{'/rooms/' + ${date}}">会議室へ</a>
        </div>
    
        <!-- 予約が出来なかった場合の処理 -->
        <p style="color: red" th:if="${error != null}" th:text="${error}"></p>
    
        <form th:object="${reservationForm}"
            th:action="@{'/reservations/' + ${date} + '/' + ${roomId}}"
            method="post">
    
            会議室: <span th:text="${room.roomName}"></span> <br />
            予約者名: <span th:text="${user.lastName + '' + user.firstName}"></span> <br />
            日付: <span th:text="${#temporals.format(date, 'yyyy/M/d')}"></span> <br />
            時間帯:<select th:field="*{startTime}">
                        <option th:each="time : ${timeList}" th:text="${time}" th:value="${time}"></option>
                   </select> <span th:if="${#fields.hasErrors('startTime')}"
                th:errors="*{startTime}" style="color: red">error!</span>
                <select th:field="*{endTime}">
                    <option th:each="time : ${timeList}" th:text="${time}"
                        th:value="${time}"></option>
                    <option th:each="time : ${timeList}" th:text="${time}"
                        th:value="${time}"></option>
                </select> <span th:if="${#fields.hasErrors('endTime')}" th:errors="*{endTime}"
                style="color: red">error!</span> <br />
            <button>予約</button>
        </form>
    
        <!-- 予約済みリスト -->
        <table>
            <tr>
                <th>時間帯</th>
                <th>予約者</th>
                <th>操作</th>
            </tr>
    
            <tr th:each="reservation : ${reservations}">
                <td><span th:text="${reservation.startTime}"></span> <span
                    th:text="${reservation.endTime}"></span></td>
                <td><span th:text="${reservation.user.lastName}"></span> <span
                    th:text="${reservation.user.firstName}"></span></td>
                <td>
                    <form th:action="@{'/reservations/' + ${date} + '/' + ${roomId}}"
                        method="post"
                        sec:authorize="${hasRole('ADMIN') or #vars.user.userId == #vars.reservation.user.userId}">
                        <input type="hidden" name="reservationId"
                            th:value="${reservation.reservationId}" /> <input type="submit"
                            name="cancel" value="取り消し" />
                    </form>
                </td>
            </tr>
        </table>
    
    </body>
    </html>
    
    
[/code]

pom.xml

[code]

    <!-- springsecurity -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
            </dependency>
            <dependency>
                <groupId>org.thymeleaf.extras</groupId>
                <artifactId>thymeleaf-extras-springsecurity5</artifactId>
            </dependency>
    
[/code]

ReservationsContoroller.java

[code]

    package mrs.app.reservation;
    
    import java.time.LocalDate;
    import java.time.LocalTime;
    import java.util.List;
    import java.util.stream.Collectors;
    import java.util.stream.Stream;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.format.annotation.DateTimeFormat;
    import org.springframework.security.access.AccessDeniedException;
    import org.springframework.security.core.annotation.AuthenticationPrincipal;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.validation.BindingResult;
    import org.springframework.validation.annotation.Validated;
    import org.springframework.web.bind.annotation.ModelAttribute;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RequestParam;
    
    import mrs.domain.model.ReservableRoom;
    import mrs.domain.model.ReservableRoomId;
    import mrs.domain.model.Reservation;
    import mrs.domain.model.User;
    import mrs.domain.service.reservation.AlreadyReservedException;
    import mrs.domain.service.reservation.ReservationService;
    import mrs.domain.service.reservation.UnavailableReservationException;
    import mrs.domain.service.room.RoomService;
    import mrs.domain.service.user.ReservationUserDetails;
    
    @Controller
    @RequestMapping("reservations/{date}/{roomId}")
    public class ReservationsController {
    
        @Autowired
        RoomService roomService;
        @Autowired
        ReservationService reservationService;
    
        //予約確認・予約一覧画面
        //いわゆる確認画面だけでまだ予約は完了していない、予約可能かの確認作業はreserve()にて
        @RequestMapping(method = RequestMethod.GET)
        String reserveForm(@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) @PathVariable("date") LocalDate date,
                @PathVariable("roomId") Integer roomId, Model model) {
    
            //指定日かつ指定会議室のreservableRoomIdを生成
            ReservableRoomId reservableRoomId = new ReservableRoomId(roomId, date);
    
            //指定日かつ指定会議室の予約リストを取得
            List<Reservation> reservations = reservationService.findReservations(reservableRoomId);
    
            //LocalDateオブジェクトを作成してリストに格納する
            List<LocalTime> timeList = Stream.iterate(LocalTime.of(0, 0), t -> t.plusMinutes(30))
                    .limit(24 * 2)
                    .collect(Collectors.toList());
    
            model.addAttribute("room", roomService.findMeetingRomm(roomId));
            model.addAttribute("reservations", reservations);
            model.addAttribute("timeList", timeList);
            //model.addAttribute("user", dummyUser());
    
            return "reservation/reserveForm";
    
        }
    
        //予約処理・予約可能かの処理
        @RequestMapping(method = RequestMethod.POST)
        String reserve(@Validated ReservationForm form, BindingResult bindingResult,
                @AuthenticationPrincipal ReservationUserDetails userDetails,
                @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) @PathVariable("date") LocalDate date,
                @PathVariable("roomId") Integer roomId, Model model) {
    
            if (bindingResult.hasErrors()) {
                return reserveForm(date, roomId, model);
            }
    
            Reservation reservation = new Reservation();
            reservation.setStartTime(form.getStartTime());
            reservation.setEndTime(form.getEndTime());
            ReservableRoom reservableRoom = new ReservableRoom(new ReservableRoomId(roomId, date));
            reservation.setReservableRoom(reservableRoom);
            //reservation.setUser(dummyUser());
            reservation.setUser(userDetails.getUser());
    
            try {
                //★ここではじめてreservable_roomに登録されているか、重複していないかをチェック
                reservationService.reserve(reservation);
    
            } catch (UnavailableReservationException | AlreadyReservedException e) {
                model.addAttribute("error", e.getMessage());
                return reserveForm(date, roomId, model);
            }
    
            return "redirect:/reservations/{date}/{roomId}";
        }
    
        //予約削除
        @RequestMapping(method = RequestMethod.POST, params = "cancel")
        String cancel(@RequestParam("reservationId") Integer reservationId,
                @AuthenticationPrincipal ReservationUserDetails userDetails,
                @PathVariable("roomId") Integer roomId,
                @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) @PathVariable("date") LocalDate date, Model model) {
    
            //ユーザー取得
            User user = userDetails.getUser();
    
            try {
                reservationService.cancel(reservationId, user);
    
            } catch (AccessDeniedException e) { //ハンドリングする例外はAccessDeniedException
                model.addAttribute("error", e.getMessage());
                return reserveForm(date, roomId, model);
            }
    
            return "redirect:/reservations/{date}/{roomId}";
        }
    
        //予約時間のデフォルトForm
        @ModelAttribute
        ReservationForm setUpForm() {
            ReservationForm form = new ReservationForm();
            //デフォルト値
            form.setStartTime(LocalTime.of(9, 0));
            form.setEndTime(LocalTime.of(10, 0));
    
            return form;
        }
    }
    
    
[/code]

WebSecurityConfig.java

[code]

    package mrs;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    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 mrs.domain.service.user.ReservationUserDetailsService;
    
    @Configuration
    
    //springsecurityのweb連帯機能(CSRF対策など)を有効にする
    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        ReservationUserDetailsService userDetailsService;
    
        @Bean
        PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .antMatchers("/js/**", "/css/**").permitAll()
                    .antMatchers("/**").authenticated()
                    .and()
                    .formLogin()
                    .loginPage("/loginForm")
                    .loginProcessingUrl("/login")
                    .usernameParameter("username")
                    .passwordParameter("password")
                    .defaultSuccessUrl("/rooms", true)
                    .failureUrl("/loginForm?error=true").permitAll()
                    ;
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            //指定のUserDetailsServiceとPasswordEncoderを使用して認証を行う
            auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
        }
    }
    
[/code]


CC BY-SA 4.0
2020-12-24T03:43:08.787
72851
2022-09-19T06:01:35.130
2020-12-24T05:55:13.623
3060
43244
question
java spring-boot thymeleaf spring-security
Spring Security で ROLE の判断が上手くいかない


質問文に記載されている情報だけでは状況を把握するのに不十分ですが、書籍
[Spring徹底入門](https://www.shoeisha.co.jp/book/detail/9784798142470)
のコードをベースに作成されていると思われましたのでそれをベースに回答します。

> デバックしたところ、RoleにはきちんとADMINがセットされています。エラーメッセージなどは出ていません。

具体的にどのように確認されたのか不明確ですが、ここで検証される値は `User` の `roleName` では無いです(無関係というわけでもないですが)。

検証の対象となるのは `ReservationUserDetails#getAuthorities()` によって得られる情報で、ここには
`ROLE_ADMIN` などが設定されることになります。

この情報について、次のようにコードを少し修正するとJavaから実際に設定されている値を確認することができます。

[code]

        // 引数を追加: @AuthenticationPrincipal ReservationUserDetails userDetails
        @RequestMapping(method = RequestMethod.GET)
        String reserveForm(@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) @PathVariable("date") LocalDate date,
            @PathVariable("roomId") Integer roomId, Model model,
            @AuthenticationPrincipal ReservationUserDetails userDetails) {
    
            // authorities に想定した権限(ROLE_ADMIN など)が含まれるか？
            Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
    
            // ...
        }
    
[/code]

* * *

(コメントを受けて追記)

`ReservationUserDetails.java`内の`getAuthorities()`実装について、サンプルコードでは次のようになっています。  
この部分が想定通り実装されていないのではないかと考えます。

[code]

        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return AuthorityUtils.createAuthorityList("ROLE_" + this.user.getRoleName().name());
        }
    
[/code]


CC BY-SA 4.0
2020-12-24T17:07:02.193
72871
2020-12-25T09:26:15.360
2020-12-25T09:26:15.360
2808
2808
72851
answer