源码地址
后端
https://github.com/jonssonyan...
前端
https://github.com/jonssonyan...
系统界面
项目结构
后端
前端
数据库
核心代码解释
ShiroConfig
Shiro核心配置类,里面有很多对象,值得好好看看,这里不一一举例,以下是该类中将用户密码使用加密存储的配置
/*
* 凭证匹配器 由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("MD5");// 散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashIterations(1024);// 散列的次数,比如散列两次,相当于MD5(MD5(""));
return hashedCredentialsMatcher;
}
UserRealm
用户登陆时的认证和授权
@Slf4j
public class UserRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private RolePermissionService rolePermissionService;
@Autowired
private PermissionService permissionService;
@Autowired
private RoleService roleService;
@Autowired
private MenuListService menuListService;
@Autowired
private RoleMenuListService roleMenuListService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 执行授权
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// 设置角色
List roles = roleService.selectRoles(SecurityUtil.getCurrentUser().getRoleId(), true);
authorizationInfo.addRoles(roles.stream().map(Role::getName).collect(Collectors.toList()));
List rolePermissions = rolePermissionService.lambdaQuery().eq(RolePermission::getRoleId, SecurityUtil.getCurrentUser().getRoleId()).list();
if (CollectionUtil.isNotEmpty(rolePermissions)) {
Set set = new HashSet<>();
for (RolePermission rolePermission : rolePermissions) {
List permissions = permissionService.lambdaQuery().eq(Permission::getId, rolePermission.getPermissionId()).list();
set.addAll(permissions);
}
// 设置权限
authorizationInfo.addStringPermissions(set.stream().map(Permission::getName).collect(Collectors.toList()));
}
return authorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {
if (authenticationToken.getPrincipal() == null) {
return null;
}
// 执行认证
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
User user = userService.selectByUsername(usernamePasswordToken.getUsername());
// 判断用户
if (user == null) {
throw new UnknownAccountException("用户不存在!");
}
if (user.getState() == 0) {
throw new DisabledAccountException("账号已被禁用!");
}
// 认证成功之后设置角色关联的菜单
List roleMenuLists = roleMenuListService.lambdaQuery().in(RoleMenuList::getRoleId, user.getRoleId()).list();
if (CollectionUtil.isNotEmpty(roleMenuLists)) {
List collect = roleMenuLists.stream().map(RoleMenuList::getMenuListId).collect(Collectors.toList());
List menuLists = menuListService.lambdaQuery().in(CollectionUtil.isNotEmpty(collect), MenuList::getId, collect).list();
// 认证成功之后设置角色关联的菜单
user.setMenuLists(CollectionUtil.isNotEmpty(collect) ? menuLists : null);
}
return new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(SystemConstants.JWT_SECRET_KEY), getName());
}
}
JWTRealm
请求接口时需要携带token,该类用于对token的认证和授权
@Slf4j
public class JWTRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private RolePermissionService rolePermissionService;
@Autowired
private PermissionService permissionService;
@Autowired
private RoleService roleService;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 执行授权
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// 设置角色
List roles = roleService.selectRoles(SecurityUtil.getCurrentUser().getRoleId(), true);
authorizationInfo.addRoles(roles.stream().map(Role::getName).collect(Collectors.toList()));
List rolePermissions = rolePermissionService.lambdaQuery().eq(RolePermission::getRoleId, SecurityUtil.getCurrentUser().getRoleId()).list();
Set set = new HashSet<>();
for (RolePermission rolePermission : rolePermissions) {
List permissions = permissionService.lambdaQuery().eq(Permission::getId, rolePermission.getPermissionId()).list();
set.addAll(permissions);
}
// 设置权限
authorizationInfo.addStringPermissions(set.stream().map(Permission::getName).collect(Collectors.toList()));
return authorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String token = (String) authenticationToken.getCredentials();
// 解密获得username,用于和数据库进行对比
String username = JwtUtils.getUsernameByToken(token);
if (StrUtil.isBlank(username)) {
throw new AuthenticationException("token认证失败!");
}
User user = userService.selectByUsername(username);
// 判断用户
if (user == null) {
throw new AuthenticationException("用户不存在!");
}
if (user.getState() == 0) {
throw new AuthenticationException("账号已被禁用!");
}
return new SimpleAuthenticationInfo(user, token, getName());
}
}
NoSessionFilter
该类用于过滤请求,获取请求中的token,并对其认证
@Slf4j
public class NoSessionFilter extends BasicHttpAuthenticationFilter {
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
HttpServletRequest servletRequest = (HttpServletRequest) request;
// 1.从Cookie获取token
String token = getTokenFromCookie(servletRequest);
if (StrUtil.isBlank(token)) {
// 2.从headers中获取
token = servletRequest.getHeader(SystemConstants.TOKEN_HEADER);
}
if (StrUtil.isBlank(token)) {
// 3.从请求参数获取
token = request.getParameter(SystemConstants.TOKEN_HEADER);
}
if (StrUtil.isBlank(token)) {
return false;
}
// 验证token
token = token.replace(SystemConstants.TOKEN_PREFIX, "");
JwtToken jwtToken = new JwtToken(token);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
// todo https://www.cnblogs.com/red-star/p/12121941.html https://blog.csdn.net/qq_43721032/article/details/110188342
try {
SecurityUtils.getSubject().login(jwtToken);
} catch (Exception e) {
return false;
}
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
PrintWriter printWriter = response.getWriter();
response.setCharacterEncoding("utf-8");
printWriter.write("403");
printWriter.flush();
printWriter.close();
return false;
}
private String getTokenFromCookie(HttpServletRequest request) {
String token = null;
Cookie[] cookies = request.getCookies();
int len = null == cookies ? 0 : cookies.length;
if (len > 0) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(SystemConstants.TOKEN_HEADER)) {
token = cookie.getValue();
break;
}
}
}
return token;
}
/**
* 对跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
}
UserController
使用Shiro的注解对接口进行角色和权限的控制
/**
* 分页查询用户
* 需要管理员权限
*
* @param userVO
* @return
*/
@PostMapping("/selectPage")
@RequiresRoles({"admin"})
@RequiresPermissions({"user:select"})
public Result
DefaultController
登陆时创建token返回给用户,同时将用户名和加密后的密码与数据库中比对
/**
* 登录
*
* @param user 登录的用户对象
* @return
*/
@PostMapping("/login")
public Result
总结
该系统使用Shrio框架对接口实现角色和权限的控制,角色权限和授权可通过用户交互界面动态实现,用户登录使用jwt单点登录,并将用户密码使用盐加密存储,从而保证了接口和用户密码的安全性。注意,不可将admin账户设置成禁用状态否则没办法登录(除了改数据库),应为状态一旦禁用只可以管理员进行解禁。每一个大型系统都会有权限的管理,权限管理在实际工作中经常遇到。