Redditアプリにロールと特権を追加する

1概要

今回の記事では、単純な役割と特権を 私たちのRedditアプリ に紹介します。そうすれば、次のような興味深いことができるようになります。通常のユーザーが毎日Redditにスケジュールできる投稿を投稿します。

また、管理者ロール、そして暗黙のうちに管理者ユーザーになるので、管理者管理領域も追加します。

2 User Role 、および Privilege エンティティ

まず、 User エンティティを変更します。リンクを介して使用します。

@Entity
public class User {
    ...

    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "users__roles",
      joinColumns = @JoinColumn(name = "user__id", referencedColumnName = "id"),
      inverseJoinColumns = @JoinColumn(name = "role__id", referencedColumnName = "id"))
    private Collection<Role> roles;

    ...
}

ユーザーと役割の関係が柔軟な多対多であることに注意してください。

次に、 Role エンティティと Privilege エンティティを定義します。その実装の詳細については、リンク/春のセキュリティ登録のためのロールと特権[Baeldungに関するこの記事]をチェックしてください。

3セットアップ

次に、これらのロールと特権を作成するために、プロジェクトのブートストラップでいくつかの基本的な設定を実行します。

private void createRoles() {
    Privilege adminReadPrivilege = createPrivilegeIfNotFound("ADMIN__READ__PRIVILEGE");
    Privilege adminWritePrivilege = createPrivilegeIfNotFound("ADMIN__WRITE__PRIVILEGE");
    Privilege postLimitedPrivilege = createPrivilegeIfNotFound("POST__LIMITED__PRIVILEGE");
    Privilege postUnlimitedPrivilege = createPrivilegeIfNotFound("POST__UNLIMITED__PRIVILEGE");

    createRoleIfNotFound("ROLE__ADMIN", Arrays.asList(adminReadPrivilege, adminWritePrivilege));
    createRoleIfNotFound("ROLE__SUPER__USER", Arrays.asList(postUnlimitedPrivilege));
    createRoleIfNotFound("ROLE__USER", Arrays.asList(postLimitedPrivilege));
}

テストユーザーを管理者にします。

private void createTestUser() {
    Role adminRole = roleRepository.findByName("ROLE__ADMIN");
    Role superUserRole = roleRepository.findByName("ROLE__SUPER__USER");
    ...
    userJohn.setRoles(Arrays.asList(adminRole, superUserRole));
}

4標準ユーザーを登録

また、 registerNewUser() 実装を介して標準ユーザーを登録するようにしてください。

@Override
public void registerNewUser(String username, String email, String password) {
    ...
    Role role = roleRepository.findByName("ROLE__USER");
    user.setRoles(Arrays.asList(role));
}

システム内の役割は次のとおりです。

  1. ROLE USER__:一般ユーザー(デフォルトロール) - これらには制限があります

1日に何件の投稿をスケジュールできるか 。 ROLE SUPER USER :スケジューリング制限なし

  1. ROLE ADMIN__:追加の管理者オプション

5プリンシパル

次に、これらの新しい権限を主要な実装に統合しましょう。

public class UserPrincipal implements UserDetails {
    ...

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
        for (Role role : user.getRoles()) {
            for (Privilege privilege : role.getPrivileges()) {
                authorities.add(new SimpleGrantedAuthority(privilege.getName()));
            }
        }
        return authorities;
    }
}

6. 標準ユーザーによる予定投稿の制限

今度は、新しいロールと特権を利用し、Redditへのスパムを避けるために、標準ユーザーが1日に3つ以上の記事をスケジュールすることを制限します。

6.1. 投稿リポジトリ

まず、 PostRepository 実装に新しい操作を追加します。これは、特定のユーザーが特定の期間にスケジュールした投稿を数えるためのものです。

public interface PostRepository extends JpaRepository<Post, Long> {
    ...

    Long countByUserAndSubmissionDateBetween(User user, Date start, Date end);

}

5.2. スケジュールされたポストコントローラ

次に、 schedule() メソッドと updatePost() メソッドの両方に簡単なチェックを追加します。

public class ScheduledPostRestController {
    private static final int LIMIT__SCHEDULED__POSTS__PER__DAY = 3;

    public Post schedule(HttpServletRequest request,...) throws ParseException {
        ...
        if (!checkIfCanSchedule(submissionDate, request)) {
            throw new InvalidDateException("Scheduling Date exceeds daily limit");
        }
        ...
    }

    private boolean checkIfCanSchedule(Date date, HttpServletRequest request) {
        if (request.isUserInRole("POST__UNLIMITED__PRIVILEGE")) {
            return true;
        }
        Date start = DateUtils.truncate(date, Calendar.DATE);
        Date end = DateUtils.addDays(start, 1);
        long count = postReopsitory.
          countByUserAndSubmissionDateBetween(getCurrentUser(), start, end);
        return count < LIMIT__SCHEDULED__POSTS__PER__DAY;
    }
}

ここでいくつか面白いことが起こっています。まず、Spring Securityと手動でやり取りし、現在ログインしているユーザーに特権があるかどうかを確認していることに注目してください。それはあなたが毎日することではありません - しかしあなたがそれをしなければならないとき、APIは非常に役に立ちます。

現在のところ、論理的には - ユーザーが POST UNLIMITED PRIVILEGE を持っている場合 - 彼らは、驚くほどスケジュールを立てることができますが、それでも多くのユーザーが選択します。

ただし、その権限がない場合、1日に最大3件の投稿をキューに入れることができます。

7. 管理者ユーザーページ

次に、役割に基づいてユーザーを明確に区別したので、小さなRedditアプリの管理者用に非常に単純なユーザー管理を実装しましょう。

7.1. すべてのユーザーを表示する

まず、システム内のすべてのユーザーをリストした基本ページを作成しましょう。

これは、すべてのユーザーをリストするためのAPIです。

@PreAuthorize("hasRole('ADMIN__READ__PRIVILEGE')")
@RequestMapping(value="/admin/users", method = RequestMethod.GET)
@ResponseBody
public List<User> getUsersList() {
    return service.getUsersList();
}

そしてサービス層の実装:

@Transactional
public List<User> getUsersList() {
    return userRepository.findAll();
}

次に、単純なフロントエンドです。

<table>
    <thead>
        <tr>
            <th>Username</th>
            <th>Roles</th>
            <th>Actions</th></tr>
    </thead>
</table>

<script>
$(function(){
    var userRoles="";
    $.get("admin/users", function(data){
        $.each(data, function( index, user ) {
            userRoles = extractRolesName(user.roles);
            $('.table').append('<tr><td>'+user.username+'</td><td>'+
              userRoles+'</td><td><a href="#" onclick="showEditModal('+
              user.id+',\''+userRoles+'\')">Modify User Roles</a></td></tr>');
        });
    });
});

function extractRolesName(roles){
    var result ="";
    $.each(roles, function( index, role ) {
        result+= role.name+" ";
    });
    return result;
}
</script>

7.2. ユーザーの役割を変更する

次に、これらのユーザーの役割を管理するための簡単なロジックをいくつか紹介します。コントローラから始めましょう:

@PreAuthorize("hasRole('USER__WRITE__PRIVILEGE')")
@RequestMapping(value = "/user/{id}", method = RequestMethod.PUT)
@ResponseStatus(HttpStatus.OK)
public void modifyUserRoles(
  @PathVariable("id") Long id,
  @RequestParam(value = "roleIds") String roleIds) {
    service.modifyUserRoles(id, roleIds);
}

@PreAuthorize("hasRole('USER__READ__PRIVILEGE')")
@RequestMapping(value = "/admin/roles", method = RequestMethod.GET)
@ResponseBody
public List<Role> getRolesList() {
    return service.getRolesList();
}

そしてサービス層:

@Transactional
public List<Role> getRolesList() {
    return roleRepository.findAll();
}
@Transactional
public void modifyUserRoles(Long userId, String ids) {
    List<Long> roleIds = new ArrayList<Long>();
    String[]arr = ids.split(",");
    for (String str : arr) {
        roleIds.add(Long.parseLong(str));
    }
    List<Role> roles = roleRepository.findAll(roleIds);
    User user = userRepository.findOne(userId);
    user.setRoles(roles);
    userRepository.save(user);
}

最後に - シンプルなフロントエンド:

<div id="myModal">
    <h4 class="modal-title">Modify User Roles</h4>
    <input type="hidden" name="id" id="userId"/>
    <div id="allRoles"></div>
    <button onclick="modifyUserRoles()">Save changes</button>
</div>

<script>
function showEditModal(userId, roleNames){
    $("#userId").val(userId);
    $.get("admin/roles", function(data){
        $.each(data, function( index, role ) {
            if(roleNames.indexOf(role.name) != -1){
                $('#allRoles').append(
                  '<input type="checkbox" name="roleIds" value="'+role.id+'" checked/> '+role.name+'<br/>')
            } else{
                $('#allRoles').append(
                  '<input type="checkbox" name="roleIds" value="'+role.id+'"/> '+role.name+'<br/>')
            }
       });
       $("#myModal").modal();
    });
}

function modifyUserRoles(){
    var roles =[];
    $.each($("input[name='roleIds']:checked"), function(){
        roles.push($(this).val());
    });
    if(roles.length == 0){
        alert("Error, at least select one role");
        return;
    }

    $.ajax({
        url: "user/"+$("#userId").val()+"?roleIds="+roles.join(","),
        type: 'PUT',
        contentType:'application/json'
        }).done(function() { window.location.href="users";
        }).fail(function(error) { alert(error.responseText);
    });
}
</script>

8セキュリティ設定

最後に、システム内の管理者ユーザーをこの新しい別のページにリダイレクトするようにセキュリティ設定を変更する必要があります。

@Autowired
private AuthenticationSuccessHandler successHandler;

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.
    ...
    .authorizeRequests()
    .antMatchers("/adminHome","/users").hasAuthority("ADMIN__READ__PRIVILEGE")
    ...
    .formLogin().successHandler(successHandler)
}

ログイン後のユーザーの着地を決定するために、カスタム認証成功ハンドラを使用しています。

@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(
      HttpServletRequest request, HttpServletResponse response, Authentication auth)
      throws IOException, ServletException {
        Set<String> privieleges = AuthorityUtils.authorityListToSet(auth.getAuthorities());
        if (privieleges.contains("ADMIN__READ__PRIVILEGE")) {
            response.sendRedirect("adminHome");
        } else {
            response.sendRedirect("home");
        }
    }
}

そして非常に単純な管理者ホームページ adminHome.html :

<html>
<body>
    <h1>Welcome, <small><span sec:authentication="principal.username">Bob</span></small></h1>
    <br/>
    <a href="users">Display Users List</a>
</body>
</html>

9結論

ケーススタディのこの新しい部分では、いくつかの単純なセキュリティアーティファクト、つまりロールと特権をアプリに追加しました。そのサポートにより、 私たちは2つの簡単な機能を構築しました - 標準ユーザーのスケジュール制限と管理ユーザーの最低限の管理者です。