本文实现了基于shiro、mybatis-plus、thymeleaf、vue、axios、hutools的基本权限管理demo,提供了用户登陆、注册、查看、锁定\解锁以及excel导出功能
本文是在上一篇shiro简单权限管理的基础上,实现:
首先,需要分别建立以下五张表,分别:
编码 | 名称 | 备注 |
---|---|---|
t_user | 用户表 | 存储用户基本信息,例如张三、李四、王五等 |
t_role | 角色表 | 存储角色基本信息,例如普通角色user,管理员角色admin |
t_permission | 权限表 | 存储权限信息,例如查看用户信息、锁定用户等 |
t_user_role_rel | 用户角色关系表 | 存储用户所属角色,支撑用户与角色的多对多关系(一个用户可拥有多个角色,一个角色可分配到多个用户) |
t_role_permission_rel | 角色权限关系表 | 存储角色拥有的操作权限,支撑角色与权限的多对多关系 |
角色建表语句如下:
drop table if exists t_user;
drop table if exists t_role;
drop table if exists t_authority;
drop table if exists t_user_role_rel;
drop table if exists t_role_authority_rel;
create table t_user
(
id bigint(20) comment '用户id',
name varchar(64) comment '用户账号',
nick_name varchar(32) comment '用户名称',
password varchar(32) comment '加密密码',
salt varchar(32) comment '密码盐值',
state int(1) comment '用户状态',
create_time timestamp comment '创建时间',
update_time timestamp comment '更新时间',
primary key (id),
unique key (name)
);
create table t_role
(
id bigint(20) comment '角色id',
name varchar(32) comment '角色名称',
code varchar(32) comment '角色编码',
create_time timestamp comment '创建时间',
update_time timestamp comment '更新时间',
primary key (id)
);
create table t_permission
(
id bigint(20) comment '权限id',
parent_id bigint(20) comment '父权限id',
name varchar(32) comment '权限名称',
type varchar(10) comment '权限类型',
code varchar(32) comment '权限编码',
create_time timestamp comment '创建时间',
update_time timestamp comment '更新时间',
primary key (id)
);
create table t_user_role_rel
(
id bigint(20) comment '用户角色关系id',
user_id bigint(20) comment '用户id',
role_id bigint(20) comment '角色id',
primary key (id)
);
create table t_role_permission_rel
(
id bigint(20) comment '角色权限关系id',
role_id bigint(20) comment '角色id',
permission_id bigint(20) comment '权限id',
primary key (id)
);
本demo主要涉及到如下依赖:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.5.3</version>
</dependency>
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.72</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.7</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
此部分主要是基于mybatis-plus,来实现相关的entity、mapper和service:
具体代码如下:
@TableName("t_user")
@Data
@Builder
public class User {
private Long id;
private String name;
private String nickName;
private String password;
private String salt;
private UserStateEnum state;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
因为要对用户状态进行管理,还需要实现用户状态枚举类,并且通过@EnumValue来申明与mybatis-plus对应的数据库枚举字段值:
@Getter
public enum UserStateEnum {
/** 锁定状态 */
LOCKED(0, "锁定"),
/** 正常状态 */
NORMAL(1, "正常");
@EnumValue
private final int key;
private final String desc;
UserStateEnum(int key, String desc) {
this.key = key;
this.desc = desc;
}
}
用户角色关系实体:
@TableName("t_user_role_rel")
@Data
@Builder
public class UserRole {
private Long id;
private Long userId;
private Long roleId;
}
角色实体:
@TableName("t_role")
@Data
public class Role {
private Long id;
private String name;
private String code;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
角色权限关系实体:
@TableName("t_role_permission_rel")
@Data
public class RolePermission {
@TableId
private Long id;
private Long roleId;
private Long permissionId;
}
权限实体:
@TableName("t_permission")
@Data
public class Permission {
private Long id;
private Long parentId;
private String name;
private String type;
private String code;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
UserMapper类没有特殊方法,直接继承即可:
public interface UserMapper extends BaseMapper<User> {
}
用户角色UserRoleMapper:
public interface UserRoleMapper extends BaseMapper<UserRole> {
}
RoleMapper因为要通过用户id查询用户的所有角色,满足shiro配置,因此增加了一个查询方法(该方法内容在对应的xml文件中,具体见后文):
public interface RoleMapper extends BaseMapper<Role> {
/**
* 根据用户id查询用户角色清单
*
* @param userId
* @return
*/
public List<Role> selectUserRoles(Long userId);
}
public interface RolePermissionMapper extends BaseMapper<RolePermission> {
}
同样,PermissionMapper因为要查询用户的全部权限,也实现了一个查询方法:
public interface PermissionMapper extends BaseMapper<Permission> {
/**
* 根据用户id查询用户权限清单
*
* @param userId
* @return
*/
public List<Permission> selectUserPermissions(Long userId);
}
一般推荐的做法是将sql写到xml配置文件中,以便进行格式化等操作。与上面mapper接口对应的两个mapper文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="pers.techlmm.shiro.advanced.mapper.RoleMapper">
<select id="selectUserRoles" parameterType="long" resultType="pers.techlmm.shiro.advanced.entity.Role">
select r.*
from t_user_role_rel ur,
t_role r
where ur.role_id = r.id
and ur.user_id = #{userId}
</select>
</mapper>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="pers.techlmm.shiro.advanced.mapper.PermissionMapper">
<select id="selectUserPermissions" parameterType="long" resultType="pers.techlmm.shiro.advanced.entity.Permission">
select p.*
from t_user_role_rel ur,
t_role_permission_rel rp,
t_permission p
where ur.role_id = rp.role_id
and p.id = rp.permission_id
and ur.user_id = #{userId}
</select>
</mapper>
完成了数据库层面的准备,接下来是提供面向上层应用的服务了。
首先是实现了UserBusiService,也就用户业务服务,实现获取全部用户信息,并封装为BO对象,具体如下:
@Service
public class UserBusiService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleMapper roleMapper;
@Autowired
private PermissionMapper permissionMapper;
@Autowired
private UserRoleMapper userRoleMapper;
@Autowired
private RoleService roleService;
@Autowired
private Snowflake snowflake;
/**
* 获取所有用户基本信息,含所属角色及权限清单
*
* @return
*/
public List<UserBO> getUserBOList() {
List<UserBO> userBOList = new ArrayList<>();
userMapper.selectList(null).forEach(user -> {
// 遍历每一个用户,并封装为BO对象
UserBO userBO = UserBO.builder()
.id(user.getId())
.name(user.getName())
.nickName(user.getNickName())
.createTime(user.getCreateTime())
.state(user.getState())
.build();
// 设置用户的角色集合
userBO.setRoles(this.getUserRoleSet(user.getId()));
// 设置用户的权限集合
userBO.setPermissions(this.getUserPermissionSet(user.getId()));
userBOList.add(userBO);
});
return userBOList;
}
public Set<String> getUserRoleSet(Long userId) {
// 基于stream操作,将每个Role对象,取出code后,归并为set
return roleMapper.selectUserRoles(userId)
.stream()
.map(Role::getCode)
.collect(Collectors.toSet());
}
public Set<String> getUserPermissionSet(Long userId) {
// 基于stream操作,将每个权限对象,归并为权限code的集合
return permissionMapper.selectUserPermissions(userId)
.stream()
.map(Permission::getCode)
.collect(Collectors.toSet());
}
public User getUserInfo(String name) {
// 按名称查询用户,返回一个结果
QueryWrapper wrapper = new QueryWrapper<>();
wrapper.eq("name", name);
return userMapper.selectOne(wrapper);
}
/**
* 添加用户,默认为普通user角色
*
* @param user
* @return
*/
@Transactional(rollbackFor = Exception.class)
public User addUser(User user) {
Role role = roleService.getRoleByCode("user");
if (role == null) {
throw new RuntimeException("以普通用户角色创建用户出现异常");
}
return this.addUser(user, role);
}
/**
* 添加用户,并赋予指定的角色
*
* @param user
* @param role
* @return
*/
@Transactional(rollbackFor = Exception.class)
public User addUser(User user, Role role) {
// 先添加用户主对象
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(LocalDateTime.now());
user.setState(UserStateEnum.NORMAL);
// 密码盐值为 随机8位字符
String salt = RandomUtil.randomString(8);
user.setSalt(salt);
// 下面的参数设定,如算法、迭代次数,要与shiro配置一致
SimpleHash hash = new SimpleHash(Md5Hash.ALGORITHM_NAME, user.getPassword(),
ByteSource.Util.bytes(salt), 1024);
// 将密码设置为加密后的base64字符串
user.setPassword(hash.toBase64());
user.setId(snowflake.nextId());
int result = userMapper.insert(user);
if (result > 0) {
// 插入用户所属角色
UserRole userRole = UserRole.builder()
.id(snowflake.nextId())
.roleId(role.getId())
.userId(user.getId())
.build();
result += userRoleMapper.insert(userRole);
}
if (result < 2) {
throw new RuntimeException("添加新注册用户出现异常");
}
return user;
}
}
对应的BO对象定义如下:
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserBO {
private Long id;
private String name;
private String nickName;
private UserStateEnum state;
private LocalDateTime createTime;
private Set<String> roles;
private Set<String> permissions;
}
鉴于锁定、解锁操作是针对用户本身,将该功能实现在UserService中,而不纳入到UserBusiService:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Transactional(rollbackFor = Exception.class)
public int lockUser(long id) {
User user = userMapper.selectById(id);
if (user == null) {
throw new RuntimeException("锁定操作异常,用户id不存在:" + id);
}
// 设置用户状态
UserStateEnum stateEnum = user.getState() == UserStateEnum.LOCKED ? UserStateEnum.NORMAL
: UserStateEnum.LOCKED;
user.setState(stateEnum);
user.setUpdateTime(LocalDateTime.now());
return userMapper.updateById(user);
}
}
另外,因为新注册用户要分配默认权限,实现如下的基于code查询权限的服务:
@Service
public class RoleService {
@Autowired
private RoleMapper roleMapper;
public Role getRoleByCode(String code) {
QueryWrapper wrapper = new QueryWrapper<>();
wrapper.eq("code", code);
return roleMapper.selectOne(wrapper);
}
}
提供了相关service服务后,接下来是着手实现相关controller等web服务功能了。
首先是LoginController,实现:
@Controller
@Slf4j
public class LoginController {
@Autowired
private UserBusiService userBusiService;
@PostMapping("/doLogin")
public String doLogin(String username, String password, String strRememberMe, Model model) {
boolean rememberMe = "on".equals(strRememberMe);
UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe);
Subject subject = SecurityUtils.getSubject();
subject.login(token);
return "redirect:/user/all";
}
@RequestMapping("/doRegister")
@ResponseBody
public CommonResult<User> doRegister(@RequestBody String form) {
JSONObject jsonForm = JSON.parseObject(form);
String userName = jsonForm.getString("username");
String nickName = jsonForm.getString("nickname");
String password = jsonForm.getString("password");
User user = User.builder().name(userName).nickName(nickName).password(password).build();
user = userBusiService.addUser(user);
if (user == null) {
return CommonResult.failed();
} else {
return CommonResult.success(user);
}
}
}
接下来是实现 UserController:
@Controller
@RequestMapping("/user")
@Slf4j
public class UserController {
@Autowired
private UserBusiService userBusiService;
@Autowired
private UserService userService;
@RequestMapping("/all")
public String getAllUser(Model model) {
List<UserBO> users = userBusiService.getUserBOList();
model.addAttribute("users", users);
return "advance/user-list";
}
@RequiresRoles("admin")
@RequiresPermissions({"userInfo:lock", "userInfo:unlock"})
@RequestMapping("/lock")
public String doLock(@RequestParam Long id) {
userService.lockUser(id);
return "redirect:/user/all";
}
@RequestMapping("/exp")
public void doExport(HttpServletRequest request, HttpServletResponse response)
throws IOException {
ExcelWriter writer = ExcelUtil.getWriter(true);
List<UserBO> users = userBusiService.getUserBOList();
writer.write(users, true);
response.setContentType(
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
response.setHeader("Content-Disposition", "attachment;filename=users.xlsx");
ServletOutputStream outputStream = response.getOutputStream();
writer.flush(outputStream, true);
writer.close();
IoUtil.close(outputStream);
}
}
为了集中处理运行时异常,实现了如下的异常类:
@ControllerAdvice
public class AppExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(AuthenticationException.class)
public ModelAndView handleAuthenticationException(AuthenticationException ex) {
ModelAndView mv = new ModelAndView("advance/login");
mv.addObject("error", ex.getMessage());
logger.warn(ex);
return mv;
}
}
基于上述异常类,因此在shiro的login出现AuthenticationException时,不需要在LoginController的doLogin中进行try catch处理,在上面集中处理集合。上文基本等同于控制器中的如下代码:
Subject subject = SecurityUtils.getSubject();
String loginError = "";
try {
// 执行登陆操作
subject.login(token);
} catch (UnknownAccountException | IncorrectCredentialsException ex) {
loginError = "用户名或密码错误";
log.warn("{}", loginError, ex);
} catch (LockedAccountException ex) {
loginError = "用户账号被锁定";
log.warn("{}", loginError, ex);
} catch (AuthenticationException ex) {
loginError = "用户账号暂不可用";
log.warn("{}", loginError, ex);
}
if (!loginError.isEmpty()) {
model.addAttribute("error", loginError);
// 登陆失败,回到登陆页
return "advance/login";
}
配置文件对于不属性框架来说,很容易因为错漏导致各种问题,此处也许特别注意。
首先实现自己的UserRealm类,实现鉴权和验证信息的获取:
public class UserRealm extends AuthorizingRealm {
@Autowired
private UserBusiService userBusiService;
/**
* 获取用户鉴权信息,也即设置用户角色和权限
*
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
User user = (User) getAvailablePrincipal(principals);
Long userId = user.getId();
authorizationInfo.setRoles(userBusiService.getUserRoleSet(userId));
authorizationInfo.setStringPermissions(userBusiService.getUserPermissionSet(userId));
return authorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String username = upToken.getUsername();
User user = userBusiService.getUserInfo(username);
if (user == null) {
throw new UnknownAccountException("用户账号不存在");
}
if (user.getState() == UserStateEnum.LOCKED) {
throw new LockedAccountException("用户账号被锁定");
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user,
user.getPassword(), ByteSource.Util.bytes(user.getSalt()), getName());
return authenticationInfo;
}
}
接下来是,实现:
相关代码如下:
@Configuration
public class ShiroConfig {
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
matcher.setHashAlgorithmName(Md5Hash.ALGORITHM_NAME);
matcher.setHashIterations(1024);
// 按base64模式
matcher.setStoredCredentialsHexEncoded(false);
return matcher;
}
@Bean
public Realm realm() {
UserRealm realm = new UserRealm();
// 主要要手工设置一下
realm.setCredentialsMatcher(hashedCredentialsMatcher());
return realm;
}
@Bean
public static DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setUsePrefix(true);
return creator;
}
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition();
chain.addPathDefinition("/login", "anon");
chain.addPathDefinition("/doLogin", "anon");
chain.addPathDefinition("/register", "anon");
chain.addPathDefinition("/doRegister", "anon");
// 静态资源不拦截
chain.addPathDefinition("/js/**", "anon");
chain.addPathDefinition("/css/**", "anon");
chain.addPathDefinition("/logout", "logout");
// 相关 filter参见 https://shiro.apache.org/web.html
chain.addPathDefinition("/**", "user");
return chain;
}
@Bean
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
@Bean
protected CacheManager cacheManager() {
return new MemoryConstrainedCacheManager();
}
}
该类很简单,就是定义了mapper的扫描路径:
@Configuration
@MapperScan("pers.techlmm.shiro.advanced.mapper")
public class MybatisPlusConfig {
}
首先是实现本dmeo的WebConfig:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("advance/login");
registry.addViewController("/register").setViewName("advance/register");
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
converters.add(converter);
}
}
其次,为了实现基于hutools的snowflake id生成,实现了如下AppConfig配置类:
@Configuration
public class AppConfig {
@Bean
public Snowflake snowflake() {
return IdUtil.createSnowflake(1, 1);
}
}
最后,为了实现通过结果的返回,实现了如下的CommonResult和ResultCode
@AllArgsConstructor
@Getter
public class CommonResult<T> {
private int code;
private String message;
private T data;
public static <T> CommonResult<T> success(T data) {
return new CommonResult<T>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getValue(),
data);
}
public static <T> CommonResult<T> success(T data, String message) {
return new CommonResult<T>(ResultCode.SUCCESS.getCode(), message, data);
}
public static <T> CommonResult<T> failed() {
return new CommonResult<T>(ResultCode.FAILED.getCode(), ResultCode.FAILED.getValue(), null);
}
public static <T> CommonResult<T> failed(String message) {
return new CommonResult<T>(ResultCode.FAILED.getCode(), message, null);
}
}
@Getter
public enum ResultCode {
/** 操作成功 */
SUCCESS(200, "操作成功"),
/** 操作失败 */
FAILED(500, "操作失败");
private int code;
private String value;
ResultCode(int code, String value) {
this.code = code;
this.value = value;
}
}
本demo是基于yaml来实现配置,并且将所有该demo的配置都写到application-ashiro.yml中,只在application.yml中指定active profile:
spring:
profiles:
active: ashiro
application-ashiro.yml具体内容如下,主要配置了:
spring:
datasource:
schema: classpath:db/shiro/schema.sql
data: classpath:db/shiro/data.sql
url: jdbc:h2:mem:test
username: root
password: test
thymeleaf:
cache: false
server:
port: 8080
servlet:
context-path: /api/v1
shiro:
loginUrl: /login
successUrl: /user/all
mybatis-plus:
type-enums-package: pers.techlmm.shiro.advanced.entity.enums
实现完所有后端功能后,就是编写前端HTML文件了。
通过该文件实现用户登陆,要点如下:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录页</title>
</head>
<body>
<h3>请登录</h3>
<form th:action="@{/doLogin}" method="post">
<div th:text="${error}"></div>
<input type="text" name="username" placeholder="用户名称"/><br/>
<input type="password" name="password" placeholder="登录密码"/><br/>
<input type="checkbox" name="rememberMe" id="remCheck"/><label for="remCheck">记住我</label><br/>
<input type="submit" value="登录"/>
<a th:href="@{/register}">注册</a>
</form>
</body>
</html>
在该文件中,实现了基于vue和axios的数据操作和交互,要点如下:
th:src="@{/js/vue.js}"
,vue.js文件防止到resources/static下,才能被访问到,并且要在shiro的filter中放开类似/js/**
的拦截。axios.defaults.baseURL = 'http://localhost:8080/api/v1';
@submit.prevent="doSubmit"
来设定响应方法并阻止事件的传递response.status === 200 && response.data.code === 200
里判断注册成功,然后通过window.location.href = '/api/v1/login'
来实现跳转到登陆页全部代码如下:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>注册用户</title>
<script type="text/javascript" th:src="@{/js/vue.js}"></script>
<!--<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>-->
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
<h3>注册新用户</h3>
<div id="app">
<span style="color: red">{{error}}</span>
<form @submit.prevent="doSubmit">
用户名称:<input type="text" placeholder="用户名称" v-model="username"/><br/>
用户昵称:<input type="text" placeholder="昵称" v-model="nickname"><br>
登录密码:<input type="password" placeholder="登录密码" v-model="password"/><br/>
确认密码:<input type="password" placeholder="再输入一次" v-model="password2"/><br/>
<button type="submit">保存并提交</button>
<button type="reset">清空表单</button>
</form>
</div>
</body>
<script>
//<![CDATA[
axios.defaults.baseURL = 'http://localhost:8080/api/v1';
var app = new Vue({
el: "#app",
data: {
username: '',
nickname: '',
password: '',
password2: '',
error: ''
},
methods: {
doSubmit: function () {
let errors = [];
if (this.username.trim().length === 0) {
errors.push("用户名称为空")
}
if (this.nickname.trim().length === 0) {
errors.push("用户昵称为空");
}
if (this.password.trim().length === 0) {
errors.push("登陆密码为空");
}
if (this.password.trim() !== this.password2.trim()) {
errors.push("两次输入密码不一致");
}
if (errors.length > 0) {
this.error = errors.join(",");
} else {
axios.post('/doRegister', {
username: this.username,
password: this.password,
nickname: this.nickname
})
.then(function (response) {
if (response.status === 200 && response.data.code === 200) {
window.alert("注册成功,请登录");
window.location.href = '/api/v1/login';
} else {
console.log(response);
}
})
.catch(function (error) {
this.error = error;
console.error(error);
})
}
}
}
});
//]]>
</script>
</html>
用户列表页面主要实现:
<shiro:principal/>
来展示当前用户信息<a th:href="@{/user/exp}">导出excel</a>
来提供导出excel功能th:each="user:${users}"
来实现对用户清单的遍历,并以table形式展示th:switch="${user.state}"
和th:case="${T(pers.techlmm.shiro.advanced.entity.enums.UserStateEnum).NORMAL}"
来实现对用户状态的枚举和判断shiro:hasAnyPermissions="userInfo:lock,userInfo:unlock"
来实现锁定、解锁权限的前端控制<a th:href="@{/logout}">退出登录</a>
提供退出登录功能全部代码如下:
<!DOCTYPE html>
<html lang="en" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>用户列表</title>
</head>
<body>
<shiro:principal/>
<a th:href="@{/user/exp}">导出excel</a>
<table cellspacing="0" border="1">
<thead>
<tr>
<th>用户名称</th>
<th>用户昵称</th>
<th>用户角色</th>
<th>用户权限</th>
<th>用户状态</th>
<th>注册时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr th:each="user:${users}">
<td th:text="${user.name}">张三</td>
<td th:text="${user.nickName}">张三</td>
<td th:text="${user.roles}"></td>
<td th:text="${user.permissions}"></td>
<td th:text="${user.state}"></td>
<td th:text="${user.createTime}"></td>
<td th:switch="${user.state}">
<a th:href="@{/user/lock(id=${user.id})}"
shiro:hasAnyPermissions="userInfo:lock,userInfo:unlock">
<span th:case="${T(pers.techlmm.shiro.advanced.entity.enums.UserStateEnum).NORMAL}">锁定
</span>
<span
th:case="${T(pers.techlmm.shiro.advanced.entity.enums.UserStateEnum).LOCKED}">解锁</span>
</a>
</td>
</tr>
</tbody>
</table>
<a th:href="@{/logout}">退出登录</a>
</body>
</html>
首先,当前demo除实现了手工注册普通用户外,其他都需要初始化:
具体初始化脚本如下:
insert into t_user
values (1, 'zhangsan', '张三', 'hroBDotL1aQX/I4ExjJ19Q==', 'c3ar6xy9', 1, now(), now());
insert into t_user
values (2, 'lisi', '李四', '9iiqNp968bPRXlJVrTCiRw==', 'r8b18roi', 1, now(), now());
insert into t_role
values (1, '普通用户', 'user', now(), now());
insert into t_role
values (2, '管理员', 'admin', now(), now());
insert into t_permission
values (1, 0, '用户管理', 'menu', 'userInfo:view', now(), now());
insert into t_permission
values (2, 1, '锁定用户', 'button', 'userInfo:lock', now(), now());
insert into t_permission
values (3, 1, '解锁用户', 'button', 'userInfo:unlock', now(), now());
insert into t_user_role_rel
values (1, 1, 1);
insert into t_user_role_rel
values (2, 2, 2);
insert into t_role_permission_rel
values (1, 1, 1);
insert into t_role_permission_rel
values (2, 2, 1);
insert into t_role_permission_rel
values (3, 2, 2);
insert into t_role_permission_rel
values (4, 2, 3);
上述初始化数据中,用户密码是经hello原始密码及随机盐值,经过md5加密后的,可通过如下代码来生成指定的数据:
// 随机生成盐
String salt = RandomUtil.randomString(8);
ByteSource bsalt = ByteSource.Util.bytes(salt);
Object password = "hello";
SimpleHash hash = new SimpleHash(Md5Hash.ALGORITHM_NAME, password, bsalt, 1024);
log.info("原始密码 {},密码盐值 {},加密密码 {}", password, salt, hash.toBase64());
具体测试模式:
1、启动服务后,访问http://localhost:8080/api/v1/login,进入登录页面
2、以zhangsan/hello登录,可查看到 用户明细,操作列为空,具体如下图:
3、以lisi登录,查看用户清单,并进行锁定、解锁操作:
4、点击列表中的导出excel链接,测试用户导出情况:
5、退出登录后,通过登录页,进入到注册页面,新注册一个用户王五:
6、以新注册王五登录后,查看用户清单:(可在下图中看到新注册用户具体信息)
为了实现在修改文件后,自动刷新而不需要重启,对于IDEA开发模式,可如下配置:
具体pom.xml改动如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
</configuration>
</plugin>
</plugins>
</build>
本demo相关所有源代码,已经开放到码云,具体地址为 https://gitee.com/coolpine/backends ,供参考,欢迎反馈相关问题和意见。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。