Fms-Java版开发实录:15 管理员修改用户权限

Fms-Java版开发实录:15 管理员修改用户权限

现在系统启动后会自动要求创建管理员,创建之后,管理员的一个核心功能就是用户管理,主要是修改用户是否能够登陆,以及权限。

现在系统启动后会自动要求创建管理员,创建之后,管理员的一个核心功能就是用户管理,主要是修改用户是否能够登陆,以及对应的Role

一些准备工作

与管理员相关的所有内容,都在各个包中新建admin包,然后创建AdminController

然后是给项目找一个主功能页面,对应的路径是/home,页面我采用了Bootstrap - Features中的样式,放在/main/home.html中,具体代码不放了,然后在MainPageController中编写/home路径对应的控制器如下:

@RequestMapping("/home")
public String homePage(Model model) {
    User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    model.addAttribute("isAdmin", user.hasRole("ADMIN"));
    return "/main/home";
}

这里用到的方法是直接从Security上下文中获取当前用户的方法,如果使用了前边的Spring Security提供的基于UserDetails的验证,这里就可以直接取出来UserDetails对象,由于我们的User对象实现了UserDetails接口,所以可以强制转型成User,这里给User类添加一个方法用于查询用户是否有某个权限:

public boolean hasRole(String roleName) {
    for (Role role : getRoles()) {
        if (role.getRole().equals(roleName)) {
            return true;
        }
    }
    return false;
}

这样就可以在/shared/navbar.html中根据当前用户是否是ADMIN角色来显示对应的菜单:

<li th:if="${isAdmin}" class="nav-item dropdown">
    <a class="nav-link dropdown-toggle" href="#" id="adminFunction" role="button"
       data-bs-toggle="dropdown" aria-expanded="false">
        系统管理
    </a>
    <ul class="dropdown-menu" aria-labelledby="adminFunction">
        <li><a class="dropdown-item" href="/admin/user" th:href="@{/admin/user}">修改用户权限</a></li>
        <li>
            <hr class="dropdown-divider">
        </li>
        <li><a class="dropdown-item" href="#">印花税设置</a></li>
        <li>
            <hr class="dropdown-divider">
        </li>
        <li><a class="dropdown-item" href="#">修改系统设置</a></li>
    </ul>
</li>

前后端不分离的话,控制器渲染页面传入的玩意是比较多,慢慢一个一个页面编写吧。至此基本上准备好了/home页面和控制器,现在准备编写修改用户权限的功能。

修改用户权限

修改用户权限是管理员的权限,所以我把对应的路径都以/admin开头。
整体的思路如下:

  1. 编写/admin/user对应的用户列表页和控制器
  2. 编写/admin/user/{id}对应的用户详情页和控制器
  3. 编写POST请求对应的/admin/user控制器,完成修改后重定向到/admin/user

用户列表页和控制器

创建/controller/admin/AdminController

@Controller
@RequestMapping("/admin")
public class AdminController {

    private final UserService userService;

    @Autowired
    public AdminController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/user")
    public String listUser(Model model, HttpServletRequest request) {
        User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        model.addAttribute("isAdmin", user.hasRole("ADMIN"));
        List<User> users = userService.getUsersNotEqual(request.getUserPrincipal().getName());
        model.addAttribute("users", users);
        return "/admin/users";
    }
}

这里先用一个/user路径来列出所有的用户及对应权限,然后可以让管理员选择修改。还要记得访问/admin及之下的路径必须要ADMIN权限才行:

.antMatchers("/admin/**").hasAuthority("ADMIN")

添加查询用户的方法

这里首先要解决列出哪些用户,我目前的设计是管理员无需修改自己的权限,所以可以在UserDao中写一个方法:

List<User> findAllByUsernameNotOrderByCreateTime(String username);

不得不说JPA真的是方便啊。有了这个方法,就可以列出某个用户以外的用户,在对应的UserService中也添加一个方法,此外也编写一个获取某个用户全部的权限名称的方法:

    @Transactional
    public List<User> getUsersNotEqual(String username) {
        return userDao.findAllByUsernameNotOrderByCreateTime(username);
    }

    @Transactional
    public List<String> getRoleNamesByUsername(String username) {
        return userDao.findByUsername(username).getRoles().stream().map(Role::getRole).collect(Collectors.toList());
    }

接下来就是要编写页面

用户列表页

很显然,这里需要用到Thymeleaf的迭代展示。这里逻辑还挺有意思,我用了一些稍微复杂一点的Thymleaf的处理。由于预估用户不会很多,暂时没打算分页,先使用一个紧凑的表格展示所有用户。这里要修改的内容也很多,罗列如下:

  1. User对象需要一个获取其详情页的方法,类似于Django中常用的get_absolute_url()方法
  2. UserService需要一个按照id查询用户的方法
  3. 编写列表页,用迭代渲染查找出的所有非当前管理员的用户

来一个一个实现如下:

absoluteUrl()方法

这个比较简单:

    public String absoluteUrl() {
        return "/admin/user/" + this.id;
    }

按照id查询用户

这个也比较简单,注意JPAfindById方法返回的是一个Optional对象,在找不到的时候直接报异常即可:

    @Transactional
    public User getUserById(int id) {
        Optional<User> user = userDao.findById(id);
        if (user.isPresent()) {
            return user.get();
        } else {
            throw new RuntimeException("用户不存在 id=" + id);
        }
    }

编写用户列表页

这个页面的核心就是用循环列出来所有用户的信息,虽然我设置了Role和用户的表是多对多关系,不过目前权限不想改的太复杂,只想每个用户对应一种Role。页面结合了迭代和th:switch的使用,如下:

<main class="container">
    <h2 class="pb-2 py-3">选择&nbsp;用户&nbsp;来修改</h2>
    <div class="row">
        <div class="col-md">
        <table class="table table-sm table-striped">
            <thead>
            <tr>
                <th>用户名</th>
                <th>登录系统</th>
                <th>权限</th>
            </tr>
            </thead>
            <tbody>
            <tr th:each="eachUser, control: ${users}">
                <td><a class="link-primary" th:href="${eachUser.absoluteUrl()}">[[${eachUser.username}]]</a></td>
                <td>
                    <i class="fas fa-check-circle text-success" th:if="${eachUser.enabled}"></i>
                    <i class="fas fa-times-circle text-danger" th:if="${!eachUser.enabled}"></i>
                </td>
                <td>
                <span th:each="eachRole : ${eachUser.roleNames}" th:switch="${eachRole}">
                    <span class="text-danger text-bo" th:case="ADMIN">管理员</span>
                    <span class="text-primary" th:case="SUPERUSER">高级用户</span>
                    <span class="text-success" th:case="USER">普通用户</span>
                </span>
                </td>
            </tr>
            </tbody>
        </table>
        </div>
        <div class="col-lg"></div>
    </div>
</main>

页面中利用了刚才编写的absoluteUrl()动态生成每个用户的详情页面地址。

用户详情页和控制器

这个比较简单,就是通过路径获取用户id,然后渲染一个编辑页面,控制器如下:

    @GetMapping("/user/{id}")
    public String userEditPage(@PathVariable(name = "id") int id, Model model) {
        User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        model.addAttribute("isAdmin", user.hasRole("ADMIN"));
        user = userService.getUserById(id);
        model.addAttribute("user", user);
        return "/admin/userEdit";
    }

其中用到的userEdit.html核心内容如下:

<main class="container">
    <h2 class="pb-2 py-3">设置用户 <span class="text-primary">[[${user.username}]]</span> 的权限</h2>
    <form action="/admin/user" th:action="@{/admin/user}" th:object="${user}" method="post">
        <input type="hidden" name="id" th:value="${user.id}">
        <hr>
        <h5>激活账户</h5>
        <div class="form-check form-switch">
            <input class="form-check-input" type="checkbox" id="enabled" name="enabled" th:checked="*{enabled}">
            <label class="form-check-label" for="enabled">可登录系统</label>
        </div>
        <h5 class="mt-4">权限设置</h5>
        <div class="form-check form-check-inline">
            <input class="form-check-input" type="radio" name="role" id="admin" value="ADMIN" th:checked="${user.hasRole('ADMIN')}">
            <label class="form-check-label" for="admin">管理员</label>
        </div>
        <div class="form-check form-check-inline">
            <input class="form-check-input" type="radio" name="role" id="superuser" value="SUPERUSER" th:checked="${user.hasRole('SUPERUSER')}">
            <label class="form-check-label" for="superuser">高级用户</label>
        </div>
        <div class="form-check form-check-inline">
            <input class="form-check-input" type="radio" name="role" id="user" value="USER" th:checked="${user.hasRole('USER')}">
            <label class="form-check-label" for="user">普通用户</label>
        </div>
        <p class="text-secondary">不建议随意赋予管理员权限</p>
        <div class="mt-4"><button type="submit" class="btn btn-primary btn-sm">提交</button></div>
    </form>
</main>

其实就是一个CHECKBOX和三个RADIO组成的页面。POST请求发送到/admin/user,接下来就是最后一步编写这个控制器。

POST请求控制器

这个控制器看似简单,其实有一个非常重要的功能,那就是在修改一个用户的权限之后,必须让其的Session立刻失效,从而导致其失去登录状态,否则就要出乱子了,为此要修改不少地方

创建SessionRegistryBean

这是一个辅助工具类,在ProjectConfig中创建:

@Bean
public SessionRegistry sessionRegistry() {
    return new SessionRegistryImpl();
}

只有这个Bean还不行,必须将其注册到Spring Security中才能操作会话,所以要在SecurityConfig继续添加配置,这里顺便也贴一下到目前为止全部的配置:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/").permitAll()
            .antMatchers("/initial/**").permitAll()
            .antMatchers("/css/**", "/img/**", "/js/**", "/webfonts/**", "/test").permitAll()
            .antMatchers("/account/register").permitAll()
            .antMatchers("/account/reset").permitAll()
            .antMatchers("/account/reset/**").permitAll()
            .antMatchers("/admin/**").hasAuthority("ADMIN")
            .antMatchers("/account/login**").permitAll()
            .anyRequest().authenticated()
            .and()
            .formLogin().loginPage("/account/login").loginProcessingUrl("/account/auth").permitAll()
            .and()
            .logout().logoutUrl("/account/signout").logoutSuccessUrl("/").permitAll()
            .and().sessionManagement().maximumSessions(1).sessionRegistry(sessionRegistry);
}

先说明一下,.antMatchers("/account/login**").permitAll()是为了在注册成功之后,返回/account/login?registered=这样一个路径,好让login.html渲染出“注册成功,请登录”的字样,修改很简单,就不放具体代码了,要点就是使用RedirectAttributes对象来拼接重定向的URL路径。

我们新添加的就是这最后一行,限制每个用户的会话数量是1,然后sessionRegistry设置进去。另外这些会在SecurityConfig中使用的Bean,都不推荐直接写在SecurityConfig中,会造成循环引用。所以我把本来放在SecurityConfig中的PasswordEncoder移动到了ProjectConfig中,这时候就能体会到Spring的精妙,无论Bean放在哪里,丝毫不影响其他用到这个Bean的代码来实施注入。

编写业务层

控制器只负责校验数据,我决定严格执行把重型业务都塞到业务层的原则。因此准备在业务层里编写一个更新用户属性,并且把用户踢掉的方法,前半段保存用户,后半段是关键,强制让对应用户的会话失效。在UserService中添加新的方法:

    @Transactional
    public User saveAndRemoveSession(User user) {
        userDao.save(user);
        for (Object userDetail : sessionRegistry.getAllPrincipals()) {
            String userName = ((User) userDetail).getUsername();
            if (userName.equals(user.getUsername())) {
                List<SessionInformation> sessionInformations = sessionRegistry.getAllSessions(userDetail, false);
                for (SessionInformation sessionInformation : sessionInformations) {
                    sessionInformation.expireNow();
                }
            }
        }
        return user;
    }

要使这段代码生效,前边的SecurityConfig中一定要设置上SessionRegistry,否则无法生效。后边这段代码其实就是找出所有有效的会话中的用户名,然后匹配要修改的用户名,如果匹配上,就用这个用户名对应的UserDetails对象,取出其对应的所有会话,然后挨个让其失效。由于之前设置了最大会话数量是1,其实也就让一个会话失效,就会踢掉所有登录的那个用户了。

编写控制器

前边的准备工作都做好了,最后才是编写控制器,逻辑很简单,先通过id取出用户,然后将获取到的属性设置到用户上,最后调用业务层的方法保存用户并踢掉用户:

@PostMapping("/user")
public String updateUser(@RequestParam(name = "id") int id,
                         @RequestParam(name = "role") String roleName,
                         @RequestParam(name = "enabled", required = false) String enabled) {

    //保存用户信息
    User user = userService.getUserById(id);
    List<Role> roles = new ArrayList<>();
    roles.add(roleDao.findByRole(roleName));
    user.setRoles(roles);
    user.setEnabled(enabled != null);

    //强制让用户登录失效
    userService.saveAndRemoveSession(user);
    return "redirect:/admin/user";
}

至此编写好了让管理员来设置用户是否能够登陆,和对应哪个Role的功能。感觉技术又进步啦。

删除用户

这个比较简单了,控制器就是接受一个POST请求:

    @PostMapping("/user/delete")
    public String deleteUser(@RequestParam(name = "id") int id) {
        User user = userService.getUserById(id);
        userService.deleteUser(user);
        return "redirect:/admin/user";
    }

在用户详情页添加一个删除按钮,然后采用BootStrapModal模态框功能,将模态框中的两个按钮外边修改为一个form元素即可,页面新增加的部分如下:

<main class="container">
   ......
            <button type="submit" class="btn btn-primary btn-sm">提交</button>
            <button type="button" class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#deleteUser">删除此用户</button>
        </div>
    </form>

    <div class="modal fade" id="deleteUser" tabindex="-1" aria-labelledby="deleteUser" aria-hidden="true">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title" id="exampleModalLabel">删除用户:[[${user.username}]] </h5>
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                </div>
                <div class="modal-body">
                    确定要删除用户 <span class="text-primary" th:text="${user.username}"></span> 吗?
                </div>
                <form class="modal-footer" th:action="@{/admin/user/delete}" method="post">
                    <input type="hidden" name="id" th:value="${user.id}">
                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">返回</button>
                    <button type="submit" class="btn btn-danger">删除</button>
                </form>
            </div>
        </div>
    </div>
</main>

最后不要忘记在业务层中添加删除方法:

    @Transactional
    public void deleteUser(User user) {
        userDao.delete(user);
    }

至此,管理员修改用户登录和权限的功能也编写完毕了。以后就非常方便了。

LICENSED UNDER CC BY-NC-SA 4.0
Comment