前面我们完成了用户模块的基本功能的开发,并且对spring boot框架提供的基本特性我们也进行了实现,进一步完善了开发骨架,后续我们只要把精力放在其他模块的业务功能的开发上。在进一步学习之前,先给小伙伴们留个作业:尝试着开发一个用户列表的查看功能,并且只有管理员角色可以查看。
UserAdminAPI
这里我们将新建一个API,而不在原来的UserAPI
中定义。因为我们要考虑未来对一些特定前缀的路径按角色授权。
像管理性质的单个实体的curd操作,我们采用标准的restful api风格来定义,以复数的形式来定义资源,以各种不同类型的请求类型来代表不同的操作(CURD)。看下我们的定义:
package com.xiaojuan.boot.web.api;
import ...
@RequestMapping("admin/users")
public interface UserAdminAPI {
@GetMapping
List<UserInfoDTO> list();
}
说明
注意,有些教程喜欢在映射的url最前面加
/
,加不加都不影响,那就去掉呗。因为我们要检查必须是已登录的管理员才能操作,原先我们要注入
session
现在我们不用这么做了,因为这个做法侵入性太大。我们可以用RequestContextHolder
的静态方法getRequestAttributes
来获得封装请求和响应对象的对象。
UserAdminController
package com.xiaojuan.boot.web.controller;
import ...
@RequiredArgsConstructor
@RestController
public class UserAdminController implements UserAdminAPI {
private final UserService userService;
@Override
public List<UserInfoDTO> list() {
ServletRequestAttributes servletReqAttrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpSession session = servletReqAttrs.getRequest().getSession();
UserInfoDTO userInfo = (UserInfoDTO) session.getAttribute(SessionConst.LOGIN_USER);
if (userInfo == null) {
throw new BusinessException("需要登录才能访问", BusinessError.NO_LOGIN.getValue());
}
userService.checkAdminRole(userInfo.getRole(), null);
return userService.listUsers();
}
}
代码说明
虽然这里比起在方法中直接注入
HttpSession
,我们要写更多的代码,但对我们方法签名的侵入性却消除了。对于这一段获取session的代码,我们可以写一个工具类来获取。要注意,这里判断登录和权限的逻辑,其实写这里并不妥,如果有很多需要认证和授权的请求方法,岂不是每个我们都要写一遍,更好的做法我们后面会采用Filter或者Interceptor来改造这块。
现在我们就来封装这个工具:
package com.xiaojuan.boot.util;
import ...
public class WebRequestUtil {
public static HttpServletRequest getRequest() {
ServletRequestAttributes servletReqAttrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (servletReqAttrs != null) return servletReqAttrs.getRequest();
return null;
}
public static HttpServletResponse getResponse() {
ServletRequestAttributes servletReqAttrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (servletReqAttrs != null) return servletReqAttrs.getResponse();
return null;
}
public static HttpSession getSession() {
HttpServletRequest request = getRequest();
if (request != null) return request.getSession();
return null;
}
public static Object getSessionAttribute(String attrName) {
HttpSession session = getSession();
if (session != null) return session.getAttribute(attrName);
return null;
}
public static void setSessionAttribute(String attrName, Object attrValue) {
HttpSession session = getSession();
if (session != null) session.setAttribute(attrName, attrValue);
}
}
这样我们的list
方法就简化了很多:
@Override
public List<UserInfoDTO> list() {
UserInfoDTO userInfo = (UserInfoDTO) WebRequestUtil.getSessionAttribute(SessionConst.LOGIN_USER);
if (userInfo == null) {
throw new BusinessException("需要登录才能访问", BusinessError.NO_LOGIN.getValue());
}
userService.checkAdminRole(userInfo.getRole(), null);
return userService.listUsers();
}
按照上面的重构方式我们将之前写的UserAPI
中的其他方法也重构下。我们发现,取消HttpServletResponse
注入的形式,我们结合全局响应拦截的机制,自然可以用void
返回值了,而不用为此特意输出一个空字符串了。
service实现
@Override
public List<UserInfoDTO> listUsers() {
SelectStatementProvider selectStatement = select(user.allColumns())
.from(user)
.where(role, isEqualTo((byte)1))
.build()
.render(RenderingStrategies.MYBATIS3);
List<User> users = userMapper.selectMany(selectStatement);
List<UserInfoDTO> result = new ArrayList<>();
for (User user : users) {
user.setPassword(null);
UserInfoDTO userInfoDTO = new UserInfoDTO();
BeanUtils.copyProperties(user, userInfoDTO);
result.add(userInfoDTO);
}
return result;
}
说明
这里我们用了mybatis dynamic sql的
selectMany
语法特性来完成满足我们需求的查询。注意,这里我们只管理所有角色为
1
的普通用户。查询结果转成DTO返回。
web单元测试
先准备测试用的数据:
-- 管理员用户,密码admin
insert into tb_user(id, username, password, role, personal_signature, create_time, update_time)
values(1, 'admin', '$2a$10$pUnRt6kmQkbIFXfJIS37Q.y8B/UyAfsMxuydOBxeo8yYirvbXiSq2', 2, null, now(), now());
insert into tb_user(id, username, password, role, personal_signature, create_time, update_time)
values(2, 'zhangsan', '$2a$10$r6YR0RZjWOKAWuiEMvjjHeV2OkVu0uQ6VbOhW5930zLaSFKsz7qp6', 1, '每天进步一点点', now(), now()); -- 密码123
insert into tb_user(id, username, password, role, personal_signature, create_time, update_time)
values(3, 'lisi', '$2a$10$RhH5P9YMYrGnqOlw/R/QAOyxQQOqWDphbT7ZaRPFg181Wu75bTk/a', 1, '笨鸟先飞', now(), now());
看下单元测试用例:
@SneakyThrows
@Test
public void testUserList() {
runner.runScript(Resources.getResourceAsReader("db/user.sql"));
// 普通用户登录访问
Map<String, List<String>> params = new HashMap<>();
params.put("username", Collections.singletonList("zhangsan"));
params.put("password", Collections.singletonList("123"));
ResponseEntity<Response<UserInfoDTO>> resp = postForm("/user/login", new TypeReference<UserInfoDTO>() {}, params);
assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK);
String cookie = resp.getHeaders().get("Set-Cookie").get(0);
Map<String, String> headerMap = new HashMap<>();
headerMap.put("Cookie", cookie);
// 没有访问权限
ResponseEntity<Response<List<UserInfoDTO>>> resp2 = get("/admin/users", new TypeReference<List<UserInfoDTO>>() {}, null, headerMap);
assertThat(resp2.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
// 用管理员登录
params.clear();
params.put("username", Collections.singletonList("admin"));
params.put("password", Collections.singletonList("admin"));
resp = postForm("/user/login", new TypeReference<UserInfoDTO>() {}, params);
assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK);
cookie = resp.getHeaders().get("Set-Cookie").get(0);
headerMap = new HashMap<>();
headerMap.put("Cookie", cookie);
resp2 = get("/admin/users", new TypeReference<List<UserInfoDTO>>() {}, null, headerMap);
assertThat(resp2.getStatusCode()).isEqualTo(HttpStatus.OK);
List<UserInfoDTO> users = resp2.getBody().getData();
assertThat(2).isEqualTo(users.size());
assertThat("每天进步一点点").isEqualTo(users.get(0).getPersonalSignature());
}
要注意,这里我们对WebTestBase
中接受泛型类型的入参做了调整,用了著名框架jackson
提供的TypeReference
类型,因为它可以处理像Response<List<UserInfoDTO>>
这样的泛型声明,而之前我们传单个class
的设计是做不到的。
最后我们把整个UserControllerTest
跑一下,ok!