分析项目:微人事

分析项目:微人事

这次我们来分析一下这个名为:”微人事“ 的项目的功能以及各个模块的实现。

这个项目分为两个大模块,分别是server模块和mail模块,这个项目的作者也已经很久没有更新了,我们先忽略掉mail这个模块,来看看server这个模块有着什么。

点击进入到server模块内,可以得知这个server模块又被切分为了四个小的模块,分别是mapper、model、service和web。

显然,这个web模块,就是我们主要学习的内容了。

之后根据经验之谈,一般在config这个包内的,基本上都是security的配置相关。

Security配置

配置中有一行:

1
2
3
4
5
6
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setAccessDecisionManager(customUrlDecisionManager);
object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);
return object;
}

来决定security的设置,而这两个设置,是被自定义的构造了出来。

这个配置主要设置了两个选择,一个是在登录的时候,根据用户名去搜索数据库,根据用户名来得到用户的权限,从而跳转到该角色所能访问的页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component
public class CustomUrlDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
for (ConfigAttribute configAttribute : configAttributes) {
String needRole = configAttribute.getAttribute();
if ("ROLE_LOGIN".equals(needRole)) {
if (authentication instanceof AnonymousAuthenticationToken) {
throw new AccessDeniedException("尚未登录,请登录!");
}else {
return;
}
}
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(needRole)) {
return;
}
}
}
throw new AccessDeniedException("权限不足,请联系管理员!");
}
}

请求分析

这个类的作用,主要是根据用户传来的请求地址,分析出请求需要的角色:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Component
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
MenuService menuService;
AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
String requestUrl = ((FilterInvocation) object).getRequestUrl();
List<Menu> menus = menuService.getAllMenusWithRole();
for (Menu menu : menus) {
if (antPathMatcher.match(menu.getUrl(), requestUrl)) {
List<Role> roles = menu.getRoles();
String[] str = new String[roles.size()];
for (int i = 0; i < roles.size(); i++) {
str[i] = roles.get(i).getName();
}
return SecurityConfig.createList(str);
}
}
return SecurityConfig.createList("ROLE_LOGIN");
}

@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}

@Override
public boolean supports(Class<?> clazz) {
return true;
}
}

还有可以值得思考的地方是,作者的security为每一个成功或者失败的Handler,都做了自己处理的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
Hr hr = (Hr) authentication.getPrincipal();
hr.setPassword(null);
RespBean ok = RespBean.ok("登录成功!", hr);
String s = new ObjectMapper().writeValueAsString(ok);
out.write(s);
out.flush();
out.close();
}
})
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException exception) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
RespBean respBean = RespBean.error("登录失败!");
if (exception instanceof LockedException) {
respBean.setMsg("账户被锁定,请联系管理员!");
} else if (exception instanceof CredentialsExpiredException) {
respBean.setMsg("密码过期,请联系管理员!");
} else if (exception instanceof AccountExpiredException) {
respBean.setMsg("账户过期,请联系管理员!");
} else if (exception instanceof DisabledException) {
respBean.setMsg("账户被禁用,请联系管理员!");
} else if (exception instanceof BadCredentialsException) {
respBean.setMsg("用户名或者密码输入错误,请重新输入!");
}
out.write(new ObjectMapper().writeValueAsString(respBean));
out.flush();
out.close();
}
})

层次的结合

如果说单单security的设置的话,可能很多人自认为自己可以耐心一点,慢慢写,也可以做到很详细的配置。但是我还看到了别的地方。

我们来看到Controller层,这里有着:

1
2
3
4
5
6
7
8
9
10
@RestController
@RequestMapping("/system/config")
public class SystemConfigController {
@Autowired
MenuService menuService;
@GetMapping("/menu")
public List<Menu> getMenusByHrId() {
return menuService.getMenusByHrId();
}
}

我们可以看出一点不太符合逻辑的东西,这里的方法明显是在展示一个菜单,为什么service的方法名有一个ByHrId?我们却没有在任何一点地方看到传入Hr的信息啊?

我们点进去看一看:

1
2
3
public List<Menu> getMenusByHrId() {
return menuMapper.getMenusByHrId(((Hr) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getId());
}

这个方法的实现是获取到security的权限表的一个ID,也就是说,它的菜单栏会根据Hr的ID在展现。

为什么要这么做呢?那是因为为了方便以后更方便扩展,可以拥有更高的权限去解锁更多的选项栏,从现在这样的一对多关系延伸到多对多关系。

规范的写法

我们还可以从Controller层看到,这里返回的是自定义的类RespBean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class RespBean {
private Integer status;
private String msg;
private Object obj;

public static RespBean build() {
return new RespBean();
}

public static RespBean ok(String msg) {
return new RespBean(200, msg, null);
}

public static RespBean ok(String msg, Object obj) {
return new RespBean(200, msg, obj);
}

public static RespBean error(String msg) {
return new RespBean(500, msg, null);
}

public static RespBean error(String msg, Object obj) {
return new RespBean(500, msg, obj);
}

//省略。。。。
}

这样可以更规范的去使用,便于让前端明白是什么类型,而不是非常粗暴的使用HashMap,使用HashMap有很多不好的地方。这里暂时不说明

自定义处理

还有,自己定义对时间的类型的处理,时间类型的绑定往往会是前后端解耦比较麻烦的地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class DateConverter implements Converter<String, Date> {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

@Override
public Date convert(String source) {
try {
return sdf.parse(source);
} catch (ParseException e) {
e.printStackTrace();
}
return null;
}
}

运用缓存

在service层上也运用了缓存的功能,我们可以使用缓存区缓解压力,就例如像菜单这样的设置,完全可以使用CacheConfig去设置一个缓存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Service
@CacheConfig(cacheNames = "menus_cache")
public class MenuService {
@Autowired
MenuMapper menuMapper;
@Autowired
MenuRoleMapper menuRoleMapper;
public List<Menu> getMenusByHrId() {
return menuMapper.getMenusByHrId(((Hr) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getId());
}

@Cacheable
public List<Menu> getAllMenusWithRole() {
return menuMapper.getAllMenusWithRole();
}

public List<Menu> getAllMenus() {
return menuMapper.getAllMenus();
}

public List<Integer> getMidsByRid(Integer rid) {
return menuMapper.getMidsByRid(rid);
}

@Transactional
public boolean updateMenuRole(Integer rid, Integer[] mids) {
menuRoleMapper.deleteByRid(rid);
if (mids == null || mids.length == 0) {
return true;
}
Integer result = menuRoleMapper.insertRecord(rid, mids);
return result==mids.length;
}
}

追溯到底层,我们可以看到GetALlMenusWithRole这样的方法的SQL语句:

1
2
3
<select id="getAllMenusWithRole" resultMap="MenuWithRole">
select m.*,r.`id` as rid,r.`name` as rname,r.`nameZh` as rnameZh from menu m,menu_role mr,role r where m.`id`=mr.`mid` and mr.`rid`=r.`id` order by m.`id`
</select>

像类似于这样的SQL语句完全可以进行一定的缓存封装,从而减少压力。

一些错误

这样项目就这么完美,没有差错吗?也不尽然,从以往的眼光来看,我们看这个项目就算是一个SSM项目的经典类型。这个项目不涉及云服务也不涉及应用监控相关的内容,但仅仅是用于自己的毕业设计的话,也就是足够了。

当然,我本身在测试这个项目的时候还发现一些这个项目错误的地方,现今指出来:

比如在使用Search功能的时候出现的keyword错误,需要把Mapper的SQL修改一下,改为 name

其次就是在使用导出数据的时候,作者还没有配置相关数据:

1
2
3
4
5
@GetMapping("/export")
public ResponseEntity<byte[]> exportData() {
List<Employee> list = (List<Employee>) employeeService.getEmployeeByPage(null, null, null,null).getData();
return POIUtils.employee2Excel(list);
}

总体来说,这个项目非常适合一些学了知识却又不知道怎么运用的初学者。