作业:开发用户列表

41 阅读4分钟

本节视频教程链接

前面我们完成了用户模块的基本功能的开发,并且对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单元测试

先准备测试用的数据:

image.png

-- 管理员用户,密码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!

image.png