前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >springboot应用-shiro增强权限管理

springboot应用-shiro增强权限管理

原创
作者头像
技术路漫漫
修改2020-07-06 10:39:20
1.3K0
修改2020-07-06 10:39:20
举报
文章被收录于专栏:技术路漫漫技术路漫漫

本文实现了基于shiro、mybatis-plus、thymeleaf、vue、axios、hutools的基本权限管理demo,提供了用户登陆、注册、查看、锁定\解锁以及excel导出功能

基本功能

本文是在上一篇shiro简单权限管理的基础上,实现:

  • 基于RBAC权限模型,建立相关数据库表,实现shiro框架与数据库的对接。
  • 基于mybatis-plus,实现相关权限查询、用户认证、新增注册用户等功能。
  • 基于thymleaf、vue、axios实现简单的前端页面展示、交互。

数据模型构建

首先,需要分别建立以下五张表,分别:

编码

名称

备注

t_user

用户表

存储用户基本信息,例如张三、李四、王五等

t_role

角色表

存储角色基本信息,例如普通角色user,管理员角色admin

t_permission

权限表

存储权限信息,例如查看用户信息、锁定用户等

t_user_role_rel

用户角色关系表

存储用户所属角色,支撑用户与角色的多对多关系(一个用户可拥有多个角色,一个角色可分配到多个用户)

t_role_permission_rel

角色权限关系表

存储角色拥有的操作权限,支撑角色与权限的多对多关系

角色建表语句如下:

代码语言:txt
复制
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主要涉及到如下依赖:

代码语言:txt
复制
<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:

Entity实体类编写

具体代码如下:

代码语言:txt
复制
@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对应的数据库枚举字段值:

代码语言:txt
复制
@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;
    }
}

用户角色关系实体:

代码语言:txt
复制
@TableName("t_user_role_rel")
@Data
@Builder
public class UserRole {

    private Long id;
    private Long userId;
    private Long roleId;
}

角色实体:

代码语言:txt
复制
@TableName("t_role")
@Data
public class Role {

    private Long id;
    private String name;
    private String code;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

角色权限关系实体:

代码语言:txt
复制
@TableName("t_role_permission_rel")
@Data
public class RolePermission {

    @TableId
    private Long id;
    private Long roleId;
    private Long permissionId;
}

权限实体:

代码语言:txt
复制
@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;
}

Mapper接口编写

UserMapper类没有特殊方法,直接继承即可:

代码语言:txt
复制
public interface UserMapper extends BaseMapper<User> {

}

用户角色UserRoleMapper:

代码语言:txt
复制
public interface UserRoleMapper extends BaseMapper<UserRole> {

}

RoleMapper因为要通过用户id查询用户的所有角色,满足shiro配置,因此增加了一个查询方法(该方法内容在对应的xml文件中,具体见后文):

代码语言:txt
复制
public interface RoleMapper extends BaseMapper<Role> {

    /**
     * 根据用户id查询用户角色清单
     *
     * @param userId
     * @return
     */
    public List<Role> selectUserRoles(Long userId);
}
代码语言:txt
复制
public interface RolePermissionMapper extends BaseMapper<RolePermission> {

}

同样,PermissionMapper因为要查询用户的全部权限,也实现了一个查询方法:

代码语言:txt
复制
public interface PermissionMapper extends BaseMapper<Permission> {

    /**
     * 根据用户id查询用户权限清单
     *
     * @param userId
     * @return
     */
    public List<Permission> selectUserPermissions(Long userId);
}

XML Mapper编写

一般推荐的做法是将sql写到xml配置文件中,以便进行格式化等操作。与上面mapper接口对应的两个mapper文件如下:

代码语言:txt
复制
<?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>
代码语言:txt
复制
<?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>

Service服务类编写

完成了数据库层面的准备,接下来是提供面向上层应用的服务了。

首先是实现了UserBusiService,也就用户业务服务,实现获取全部用户信息,并封装为BO对象,具体如下:

代码语言:txt
复制
@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对象定义如下:

代码语言:txt
复制
@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:

代码语言:txt
复制
@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查询权限的服务:

代码语言:txt
复制
@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);
    }
}

后台web功能编写

提供了相关service服务后,接下来是着手实现相关controller等web服务功能了。

控制器编写

首先是LoginController,实现:

  • 实现doLogin逻辑,完成用户登陆。
  • 实现doRegister逻辑,完成新用户注册。
代码语言:txt
复制
@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:

  • 实现getAllUser,查询所有用户信息,并封装为可供前端展示的BO对象列表。
  • 实现doLock逻辑,实现对用户的解锁、锁定操作,并且通过@RequiresRoles等注解,对该api进行权限管理。
  • 实现doExport逻辑,基于hutools的工具类,以及apache-poi,实现简单的用户列表导出excel。
代码语言:txt
复制
@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);
    }
}

异常处理类编写

为了集中处理运行时异常,实现了如下的异常类:

代码语言:txt
复制
@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处理,在上面集中处理集合。上文基本等同于控制器中的如下代码:

代码语言:txt
复制
        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";
        }

配置类实现

配置文件对于不属性框架来说,很容易因为错漏导致各种问题,此处也许特别注意。

Shiro相关配置类

首先实现自己的UserRealm类,实现鉴权和验证信息的获取:

代码语言:txt
复制
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;
    }
}

接下来是,实现:

  • 实例化HashedCredentialsMatcher,定义相关加密算法等信息。
  • 实例化Realm,特别注意要将上述HashedCredentialsMatcher的也设置到该示例中。
  • 定义ShiroFilterChainDefinition,添加相关URL拦截规则。
  • 实例化ShiroDialect,因为前段页面有用到基于thymeleaf-extras-shiro的权限便签,例如shiro:hasAnyPermissions
  • 实例化DefaultAdvisorAutoProxyCreator,从相关资料看是为了解决相关bug,具体没验证

相关代码如下:

代码语言:txt
复制
@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();
    }
}

Mybatis-Plus配置类

该类很简单,就是定义了mapper的扫描路径:

代码语言:txt
复制
@Configuration
@MapperScan("pers.techlmm.shiro.advanced.mapper")
public class MybatisPlusConfig {

}

其他配置类

首先是实现本dmeo的WebConfig:

  • 添加基于FastJsonHttpMessageConverter的messageConverters,因为基本都是采用非restful模式,但注册用户是采用的responsebody模式,通过该converter实现doRegister返回结果中的CommonResult实体正常转换为json对象返回前端。
  • 通过addViewControllers,添加login和register两个前端视图,从而无需实现无业务含义的请求转发。
代码语言:txt
复制
@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配置类:

代码语言:txt
复制
@Configuration
public class AppConfig {

    @Bean
    public Snowflake snowflake() {
        return IdUtil.createSnowflake(1, 1);
    }
}

通用类实现

最后,为了实现通过结果的返回,实现了如下的CommonResult和ResultCode

代码语言:txt
复制
@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);
    }
}
代码语言:txt
复制
@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;
    }
}

application配置文件

本demo是基于yaml来实现配置,并且将所有该demo的配置都写到application-ashiro.yml中,只在application.yml中指定active profile:

代码语言:txt
复制
spring:
  profiles:
    active: ashiro

application-ashiro.yml具体内容如下,主要配置了:

  • 初始化数据库脚本
  • thymeleaf模板文件不缓存
  • 设置了context-path
  • 设置了shiro相关的loginUrl和successUrl
  • 最后,特别需要说明,上面UserStateEnum类,需要在mybatis-plus中手工配置一下enums的包路径。
代码语言:txt
复制
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模板文件实现

实现完所有后端功能后,就是编写前端HTML文件了。

login.html实现

通过该文件实现用户登陆,要点如下:

  • form表单的action,可以采用th标签及thymeleaf@url模式,实现自动管理baseurl,例如 th:action="@{/doLogin}",因为我在后端配置了统一的context url 为/api/v1,采用该模式可以不耦合url。
  • 实现一个不太完善的rememberMe功能。
代码语言:txt
复制
<!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>

register.html实现

在该文件中,实现了基于vue和axios的数据操作和交互,要点如下:

  • 引入vue.js文件时,需要基于thymeleaf规范,例如th:src="@{/js/vue.js}",vue.js文件防止到resources/static下,才能被访问到,并且要在shiro的filter中放开类似/js/**的拦截。
  • 基于axios提交请求时,主要要设置baseURL,例如axios.defaults.baseURL = 'http://localhost:8080/api/v1';
  • 因为form中submit默认会提交并刷新页面,所以要通过@submit.prevent="doSubmit"来设定响应方法并阻止事件的传递
  • 当前是在axios请求的响应中,通过response.status === 200 && response.data.code === 200里判断注册成功,然后通过window.location.href = '/api/v1/login'来实现跳转到登陆页

全部代码如下:

代码语言:txt
复制
<!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>

user-list.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>提供退出登录功能

全部代码如下:

代码语言:txt
复制
<!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除实现了手工注册普通用户外,其他都需要初始化:

  • 初始化zhangsan、lisi两个用户。
  • 初始化user、admin两个角色。
  • 初始化用户查看、锁定用户、解锁用户三个权限。
  • 初始化两个关系表。

具体初始化脚本如下:

代码语言:txt
复制
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加密后的,可通过如下代码来生成指定的数据:

代码语言:txt
复制
// 随机生成盐
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登录,可查看到 用户明细,操作列为空,具体如下图:

user-list
user-list

3、以lisi登录,查看用户清单,并进行锁定、解锁操作:

admin-lock
admin-lock

4、点击列表中的导出excel链接,测试用户导出情况:

export
export

5、退出登录后,通过登录页,进入到注册页面,新注册一个用户王五:

register
register

6、以新注册王五登录后,查看用户清单:(可在下图中看到新注册用户具体信息)

register-list
register-list

补充说明

热部署调试配置

为了实现在修改文件后,自动刷新而不需要重启,对于IDEA开发模式,可如下配置:

  • 引入 spring-boot-devtools依赖,并且设置spring-boot-maven-plugin的fork配置
  • 在application配置文件中,将thymeleaf的cache属性是指为false
  • 如果需要自动体现,可通过saveaction插件的build actions中的 compile files属性。
  • 如果不需要自动体现,可手工在修改了文件后,通过ctr+shift+f9重新编译当前文件,通过ctrl+f9重新编译整个工程来体现。

具体pom.xml改动如下:

代码语言:txt
复制
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <optional>true</optional>
</dependency>
代码语言:txt
复制
<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 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 基本功能
  • 数据模型构建
  • 依赖引入
  • 后台数据模型服务编写
    • Entity实体类编写
      • Mapper接口编写
        • XML Mapper编写
          • Service服务类编写
          • 后台web功能编写
            • 控制器编写
              • 异常处理类编写
              • 配置类实现
                • Shiro相关配置类
                  • Mybatis-Plus配置类
                    • 其他配置类
                      • 通用类实现
                      • application配置文件
                      • 前端HTML模板文件实现
                        • login.html实现
                          • register.html实现
                            • user-list.html实现
                            • 测试验证
                              • 初始化数据
                                • 效果测试
                                • 补充说明
                                  • 热部署调试配置
                                    • 源代码下载
                                      • 参考资料
                                      领券
                                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档