现在系统启动后会自动要求创建管理员,创建之后,管理员的一个核心功能就是用户管理,主要是修改用户是否能够登陆,以及对应的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
开头。
整体的思路如下:
- 编写
/admin/user
对应的用户列表页和控制器 - 编写
/admin/user/{id}
对应的用户详情页和控制器 - 编写
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
的处理。由于预估用户不会很多,暂时没打算分页,先使用一个紧凑的表格展示所有用户。这里要修改的内容也很多,罗列如下:
User
对象需要一个获取其详情页的方法,类似于Django
中常用的get_absolute_url()
方法UserService
需要一个按照id
查询用户的方法- 编写列表页,用迭代渲染查找出的所有非当前管理员的用户
来一个一个实现如下:
absoluteUrl()
方法
这个比较简单:
public String absoluteUrl() {
return "/admin/user/" + this.id;
}
按照id
查询用户
这个也比较简单,注意JPA
的findById
方法返回的是一个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">选择 用户 来修改</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
立刻失效,从而导致其失去登录状态,否则就要出乱子了,为此要修改不少地方
创建SessionRegistry
的Bean
这是一个辅助工具类,在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";
}
在用户详情页添加一个删除按钮,然后采用BootStrap
的Modal
模态框功能,将模态框中的两个按钮外边修改为一个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);
}
至此,管理员修改用户登录和权限的功能也编写完毕了。以后就非常方便了。