第八天:验证码、菜单、角色功能实现

一.验证码功能实现

(一)使用google kaptcha生成验证码

1.添加验证码配置类

新建yeb/yeb-server/src/main/java/com/cxy/server/config/CaptchaConfig.java

package com.cxy.server.config;

import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Properties;

/**
 * @author 陈鑫元
 * @description 验证码配置类
 * @date 2021-05-24 18:00
 * @since 1.0.0
 */
@Configuration
public class CaptchaConfig {
    @Bean
    public DefaultKaptcha defaultKaptcha() {
        // 验证码生成器
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        // 配置
        Properties properties = new Properties();
        // 是否有边框
        properties.setProperty("kaptcha.border", "yes");
        // 设置边框颜色
        //properties.setProperty("kaptcha.border.color","105,179,90");
        properties.setProperty("kaptcha.border.color", "224,224,224");
        // 边框粗细度,默认为1
        properties.setProperty("kaptcha.border.thickness", "1");
        // 设置验证码文本字符颜色,默认黑色
        //properties.setProperty("kaptcha.textproducer.font.color", "blue");
        // 设置字体样式
        properties.setProperty("kaptcha.textproducer.font.names", "微软雅黑");
        //properties.setProperty("kaptcha.textproducer.font.names", "宋体,楷体,微软雅黑");
        // 验证码 session key
        properties.setProperty("kaptcha.session.key", "code");
        // 验证码图片宽度,默认为 200
        properties.setProperty("kaptcha.image.width", "100");
        // 验证码图片高度,默认为40
        properties.setProperty("kaptcha.image.height", "40");
        // 字体大小
        properties.setProperty("kaptcha.textproducer.font.size", "30");
        // 验证码长度
        properties.setProperty("kaptcha.textproducer.char.length", "4");
        // 没有干扰
        properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.NoNoise");

        Config config = new Config(properties);
        defaultKaptcha.setConfig(config);

        return defaultKaptcha;
    }
}
2.写验证码接口

新建yeb/yeb-server/src/main/java/com/cxy/server/controller/CaptchaController.java

package com.cxy.server.controller;

import com.google.code.kaptcha.impl.DefaultKaptcha;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.IOException;

/**
 * @author 陈鑫元
 * @description 验证码接口
 * @date 2021-05-24 18:03
 * @since 1.0.0
 */
@RestController
public class CaptchaController {
    @Autowired
    private DefaultKaptcha defaultKaptcha;

    @ApiOperation(value = "验证码")
    @GetMapping(value = "/captcha", produces = "image/jpeg")
    public void captcha(HttpServletRequest request, HttpServletResponse response) {
        // 定义 response 输出类型为 image/jpeg 类型
        response.setDateHeader("Expires", 0);
        response.setHeader("Cache-Control", "no-store,no-cache,must-revalidate");
        response.addHeader("Cache-Control", "post-check=0,pre-check=0");
        response.setHeader("Pragma", "no-cache");
        response.setContentType("image/jpeg");
        // 生成验证码 开始
        String text = defaultKaptcha.createText(); // 获取验证码文本内容
        System.out.println("验证码文本内容:" + text);
        request.getSession().setAttribute("captcah", text);
        BufferedImage image = defaultKaptcha.createImage(text); // 根据文本内容创建图形验证码
        ServletOutputStream outputStream = null;
        try {
            outputStream = response.getOutputStream();
            ImageIO.write(image, "jpg", outputStream); // 输出流输出图片,格式为jpg
            outputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (null != outputStream) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        // 生成验证码 结束
        // 查看验证码: http://localhost:8081/captcha
    }
}

(二)校验验证码

1.登录实体类添加验证码字段

修改yeb/yeb-server/src/main/java/com/cxy/server/pojo/AdminLoginParam.java

image.png

2.登录Service添加验证码字段

修改yeb/yeb-server/src/main/java/com/cxy/server/service/IAdminService.java

image.png

3.登录Service实现类实现校验验证码功能

修改yeb/yeb-server/src/main/java/com/cxy/server/service/impl/AdminServiceImpl.java

package com.cxy.server.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cxy.server.config.security.component.JwtTokenUtil;
import com.cxy.server.mapper.AdminMapper;
import com.cxy.server.pojo.Admin;
import com.cxy.server.service.IAdminService;
import com.cxy.server.utils.RespBean;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 陈鑫元
 * @since 2021-05-21
 */
@Service
public class AdminServiceImpl extends ServiceImpl<AdminMapper, Admin> implements IAdminService {
    @Autowired
    private UserDetailsService userDetailsService; // 权限框架的

    @Autowired
    private PasswordEncoder passwordEncoder; // 安全框架-密码加密解密

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Value("${jwt.tokenHead}")
    private String tokenHead; // token 头部信息

    @Autowired
    private AdminMapper adminMapper;

    /**
     * 登录之后返回 token
     *
     * @param username
     * @param password
     * @param code
     * @param request
     * @return
     */
    @Override
    public RespBean login(String username, String password, String code, HttpServletRequest request) {
        // 校验验证码
        String captcha = (String) request.getSession().getAttribute("captcah");
        if (StringUtils.isEmpty(code) || !captcha.equalsIgnoreCase(code)) {
            return RespBean.error("验证码输入错误,请重新输入!");
        }
        // 登录
        UserDetails userDetails = userDetailsService.loadUserByUsername(username); // 调用权限框架方法获取用户名
        // passwordEncoder参数:第一个用户传过来的密码,第二个从 userDetails 中获取的密码
        if (null == userDetails || !passwordEncoder.matches(password, userDetails.getPassword())) {
            return RespBean.error("用户名或密码不正确");
        }
        if (!userDetails.isEnabled()) {
            return RespBean.error("账号被禁用,请联系管理员!");
        }
        // 更新 security 登录用户对象,设置到全局
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails
                , null, userDetails.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        // 登录成功,生成 token
        String token = jwtTokenUtil.generateToken(userDetails);
        Map<String, String> tokenMap = new HashMap<>();
        tokenMap.put("tokenHead", tokenHead);
        tokenMap.put("token", token);
        return RespBean.success("登录成功", tokenMap);
    }

    /**
     * 根据用户名获取用户
     *
     * @param username
     * @return
     */
    @Override
    public Admin getAdminByUserName(String username) {
        // 要作空判断,这里为了简单直接返回
        return adminMapper.selectOne(new QueryWrapper<Admin>().eq("username", username).eq("enabled", true));
    }
}

(三)测试接口

image.png

二.菜单功能实现

(一)根据用户id查询菜单列表

1.修改菜单实体类(在菜单类里添加子菜单属性)

修改yeb/yeb-server/src/main/java/com/cxy/server/pojo/Menu.java文件

image.png

2.修改菜单接口

修改:yeb/yeb-server/src/main/java/com/cxy/server/controller/MenuController.java文件

package com.cxy.server.controller;


import com.cxy.server.pojo.Menu;
import com.cxy.server.service.IMenuService;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * <p>
 * 前端控制器
 * </p>
 *
 * @author 陈鑫元
 * @since 2021-05-21
 */
@RestController
@RequestMapping("/system/cfg")
public class MenuController {
    @Autowired
    private IMenuService menuService;

    @ApiOperation(value = "通过用户id查询菜单列表")
    @GetMapping("/menu")
    public List<Menu> getMenusByAdminId() {
        // 只要登录,用户信息存在 security 全局对象中,从全局对象中获取用户id
        return menuService.getMenusByAdminId();
    }
}
3.修改菜单Service

修改:yeb/yeb-server/src/main/java/com/cxy/server/service/IMenuService.java文件

package com.cxy.server.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.cxy.server.pojo.Menu;

import java.util.List;

/**
 * <p>
 * 服务类
 * </p>
 *
 * @author 陈鑫元
 * @since 2021-05-21
 */
public interface IMenuService extends IService<Menu> {
    /**
     * 通过用户id查询菜单列表
     *
     * @return
     */
    List<Menu> getMenusByAdminId();
}
4.修改菜单Service实现类以实现根据管理员id查询菜单列表功能

修改:yeb/yeb-server/src/main/java/com/cxy/server/service/impl/MenuServiceImpl.java文件

package com.cxy.server.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cxy.server.mapper.MenuMapper;
import com.cxy.server.pojo.Admin;
import com.cxy.server.pojo.Menu;
import com.cxy.server.service.IMenuService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 陈鑫元
 * @since 2021-05-21
 */
@Service
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements IMenuService {
    @Autowired
    private MenuMapper menuMapper;

    /**
     * 通过用户id查询菜单列表
     *
     * @return
     */
    @Override
    public List<Menu> getMenusByAdminId() {
        return menuMapper.getMenusByAdminId(((Admin) SecurityContextHolder
                .getContext().getAuthentication().getPrincipal()).getId());
    }
}
5.修改菜单Mapper

修改:yeb/yeb-server/src/main/java/com/cxy/server/mapper/MenuMapper.java文件

package com.cxy.server.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cxy.server.pojo.Menu;

import java.util.List;

/**
 * <p>
 * Mapper 接口
 * </p>
 *
 * @author 陈鑫元
 * @since 2021-05-21
 */
public interface MenuMapper extends BaseMapper<Menu> {
    /**
     * 通过用户id查询菜单列表
     *
     * @param id
     * @return
     */
    List<Menu> getMenusByAdminId(Integer id);
}
6.修改xml中的sql语句

修改:yeb/yeb-server/src/main/resources/mapper/MenuMapper.xml文件

<?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="com.cxy.server.mapper.MenuMapper">

    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="com.cxy.server.pojo.Menu">
        <id column="id" property="id"/>
        <result column="url" property="url"/>
        <result column="path" property="path"/>
        <result column="component" property="component"/>
        <result column="name" property="name"/>
        <result column="iconCls" property="iconCls"/>
        <result column="keepAlive" property="keepAlive"/>
        <result column="requireAuth" property="requireAuth"/>
        <result column="parentId" property="parentId"/>
        <result column="enabled" property="enabled"/>
    </resultMap>

    <resultMap id="Menus" type="com.cxy.server.pojo.Menu" extends="BaseResultMap">
        <collection property="children" ofType="com.cxy.server.pojo.Menu">
            <id column="id2" property="id"/>
            <result column="url2" property="url"/>
            <result column="path2" property="path"/>
            <result column="component2" property="component"/>
            <result column="name2" property="name"/>
            <result column="iconCls2" property="iconCls"/>
            <result column="keepAlive2" property="keepAlive"/>
            <result column="requireAuth2" property="requireAuth"/>
            <result column="parentId2" property="parentId"/>
            <result column="enabled2" property="enabled"/>
        </collection>
    </resultMap>

    <!-- 通用查询结果列 -->
    <sql id="Base_Column_List">
        id, url, path, component, name, iconCls, keepAlive, requireAuth, parentId, enabled
    </sql>

    <!-- 根据用户 id 查询菜单列表 -->
    <select id="getMenusByAdminId" resultMap="Menus">
        SELECT DISTINCT
        m1.*,
        m2.id          AS id2,
        m2.url         AS url2,
        m2.path        AS path2,
        m2.component   AS component2,
        m2.name        AS name2,
        m2.iconcls     AS iconCls2,
        m2.keepalive   AS keepAlive2,
        m2.requireauth AS requireAuth2,
        m2.parentid    AS parentId2,
        m2.enabled     AS enabled2
        FROM
        t_menu m1,
        t_menu m2,
        t_admin_role ar,
        t_menu_role mr
        WHERE
        m1.id = m2.parentid
        AND
        m2.id = mr.mid
        AND
        mr.rid = ar.rid
        AND
        ar.adminid = #{id}
        AND
        m2.enabled=TRUE
        ORDER BY
        m2.id
    </select>

</mapper>

(二)测试接口

image.png

(三)使用Redis存放菜单数据

1.配置Redis工具类

新建yeb/yeb-server/src/main/java/com/cxy/server/config/RedisConfig.java文件:

package com.cxy.server.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * @author 陈鑫元
 * @description Redis 配置类
 * @date 2021-05-26 12:01
 * @since 1.0.0
 */
@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        // String 类型 Key 序列器
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        // Json 类型 Value 序列器
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        // String 类型 HashKey 序列器
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        // Json 类型 HashValue 序列器
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setConnectionFactory(connectionFactory);
        return redisTemplate;
    }
}
2.修改菜单Service实现类从Redis存取数据

修改:yeb/yeb-server/src/main/java/com/cxy/server/service/impl/MenuServiceImpl.java文件

package com.cxy.server.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cxy.server.mapper.MenuMapper;
import com.cxy.server.pojo.Admin;
import com.cxy.server.pojo.Menu;
import com.cxy.server.service.IMenuService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.util.List;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 陈鑫元
 * @since 2021-05-21
 */
@Service
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements IMenuService {
    @Autowired
    private MenuMapper menuMapper;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 通过用户id查询菜单列表
     *
     * @return
     */
    @Override
    public List<Menu> getMenusByAdminId() {
        Integer adminId = ((Admin) SecurityContextHolder
                .getContext().getAuthentication().getPrincipal()).getId();
        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
        // 从 redis 获取菜单数据
        @SuppressWarnings("unchecked")
        List<Menu> menus = (List<Menu>) valueOperations.get("menu_" + adminId);
        // 如果为空,去数据库获取
        if (CollectionUtils.isEmpty(menus)) {
            menus = menuMapper.getMenusByAdminId(adminId);
            // 将数据设置到 Redis 中
            valueOperations.set("menu_" + adminId, menus);
        }
        return menus;
    }
}
3.测试

第一次查询时,Redis并没有菜单数据

image.png

会从数据库中查询菜单数据并设置到Redis中,此时再次查看发现Redis中已经有数据。再次查询会直接查询Redis中数据。

image.png
image.png

三.角色功能实现

(一)权限管理

1.权限管理RBAC基本概念

RBAC是基于角色的访问控制( Role-Based Access Control)在RBAC中,权限与角色相关联,用户通过扮演适当的角色从而得到这些角色的权限。这样管理都是层级相互依赖的,权限赋予给角色,角色又赋予用户,这样的权限设计很清楚,管理起来很方便。
RBAC授权实际上是 WhoWhatHow 三元组之间的关系,也就是 WhoWhat 进行 How 的操作,简单说明就是谁对什么资源做了怎样的操作。

2.RBAC表结构设计

实体对应关系
用户-角色-资源实体间对应关系图分析如下:

image.png

这里用户与角色实体对应关系为多对多,角色与资源对应关系同样为多对多关系,所以在实体设计上用户与角色间增加用户角色实体,将多对多的对应关系拆分为一对多,同理,角色与资源多对多对应关系拆分出中间实体对象权限实体。

3.表结构设计

从上面实体对应关系分析,权限表设计分为以下基本的五张表结构:用户表(admin),角色表(role),用户角色表(admin_role),菜单表(menu),菜单权限表(menu_role),表结构关系如下:

image.png

(二)根据请求url判断角色

1.修改菜单实体类(在菜单类里添加角色列表属性)

修改yeb/yeb-server/src/main/java/com/cxy/server/pojo/Menu.java文件

image.png

2.修改菜单Service

修改:yeb/yeb-server/src/main/java/com/cxy/server/service/IMenuService.java文件

image.png

3.修改菜单Service实现类

修改:yeb/yeb-server/src/main/java/com/cxy/server/service/impl/MenuServiceImpl.java文件

image.png

4.修改菜单Mapper

修改:yeb/yeb-server/src/main/java/com/cxy/server/mapper/MenuMapper.java文件

image.png

5.修改菜单xml

修改:yeb/yeb-server/src/main/java/mapper/MenuMapper.xml文件

<?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="com.cxy.server.mapper.MenuMapper">

    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="com.cxy.server.pojo.Menu">
        <id column="id" property="id"/>
        <result column="url" property="url"/>
        <result column="path" property="path"/>
        <result column="component" property="component"/>
        <result column="name" property="name"/>
        <result column="iconCls" property="iconCls"/>
        <result column="keepAlive" property="keepAlive"/>
        <result column="requireAuth" property="requireAuth"/>
        <result column="parentId" property="parentId"/>
        <result column="enabled" property="enabled"/>
    </resultMap>

    <resultMap id="Menus" type="com.cxy.server.pojo.Menu" extends="BaseResultMap">
        <collection property="children" ofType="com.cxy.server.pojo.Menu">
            <id column="id2" property="id"/>
            <result column="url2" property="url"/>
            <result column="path2" property="path"/>
            <result column="component2" property="component"/>
            <result column="name2" property="name"/>
            <result column="iconCls2" property="iconCls"/>
            <result column="keepAlive2" property="keepAlive"/>
            <result column="requireAuth2" property="requireAuth"/>
            <result column="parentId2" property="parentId"/>
            <result column="enabled2" property="enabled"/>
        </collection>
    </resultMap>
    <resultMap id="MenusWithMap" type="com.cxy.server.pojo.Menu" extends="BaseResultMap">
        <collection property="roles" ofType="com.cxy.server.pojo.Role">
            <id column="rid" property="id"/>
            <result column="rname" property="name"/>
            <result column="rnameZh" property="nameZh"/>
        </collection>
    </resultMap>

    <!-- 通用查询结果列 -->
    <sql id="Base_Column_List">
        id, url, path, component, name, iconCls, keepAlive, requireAuth, parentId, enabled
    </sql>

    <!-- 根据用户 id 查询菜单列表 -->
    <select id="getMenusByAdminId" resultMap="Menus">
        SELECT DISTINCT
            m1.*, m2.id AS id2,
            m2.url AS url2,
            m2.path AS path2,
            m2.component AS component2,
            m2. NAME AS name2,
            m2.iconcls AS iconCls2,
            m2.keepalive AS keepAlive2,
            m2.requireauth AS requireAuth2,
            m2.parentid AS parentId2,
            m2.enabled AS enabled2
        FROM
            t_menu m1,
            t_menu m2,
            t_admin_role ar,
            t_menu_role mr
        WHERE
            m1.id = m2.parentid
        AND m2.id = mr.mid
        AND mr.rid = ar.rid
        AND ar.adminid = #{id}
        AND m2.enabled = TRUE
        ORDER BY
            m2.id
    </select>

    <!-- 通过角色获取菜单列表 -->
    <select id="getMenusWithRole" resultMap="MenusWithRole">
        SELECT
            m.id,
            m.url,
            m.path,
            m.component,
            m. NAME,
            m.iconCls,
            m.keepAlive,
            m.requireAuth,
            m.parentId,
            m.enabled,
            r.id AS rid,
            r. NAME AS rname,
            r.nameZh AS nameZh
        FROM
            t_menu m,
            t_menu_role mr,
            t_role r
        WHERE
            m.id = mr.mid
        AND r.id = mr.rid
        ORDER BY
            m.id
    </select>
</mapper>

(二)判断用户登录角色

判断登录的用户都有那些角色,并和跟据请求url判断角色进行比较,如果有一致的,则是合法访问(用户可以访问此菜单资源),否则为非法访问(用户不可以访问此菜单资源)。

1.修改管理员实体类

修改:yeb/yeb-server/src/main/java/com/cxy/server/pojo/Admin.java文件

package com.cxy.server.pojo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AccessLevel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.experimental.Accessors;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

/**
 * <p>
 * 管理员表
 * </p>
 *
 * @author 陈鑫元
 * @since 2021-05-21
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("t_admin")
@ApiModel(value = "Admin对象", description = "管理员表")
public class Admin implements Serializable, UserDetails {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "id")
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    @ApiModelProperty(value = "姓名")
    private String name;

    @ApiModelProperty(value = "手机号码")
    private String phone;

    @ApiModelProperty(value = "住宅电话")
    private String telephone;

    @ApiModelProperty(value = "联系地址")
    private String address;

    @ApiModelProperty(value = "是否启用")
    @Getter(AccessLevel.NONE) // 不需要生成 get 方法,防止与 UserDetails 重写的 isEnabled 冲突
    private Boolean enabled;

    @ApiModelProperty(value = "用户名")
    private String username;

    @ApiModelProperty(value = "密码")
    private String password;

    @ApiModelProperty(value = "用户头像")
    private String userFace;

    @ApiModelProperty(value = "备注")
    private String remark;

    @ApiModelProperty(value = "角色")
    @TableField(exist = false)
    private List<Role> roles;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities = roles
                .stream()
                .map(role -> new SimpleGrantedAuthority(role.getName()))
                .collect(Collectors.toList());
        return authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return this.enabled;
    }
}
2.修改管理员Service

修改:yeb/yeb-server/src/main/java/com/cxy/server/service/IAdminService.java文件

image.png

3.修改管理员ServiceImpl

修改:yeb/yeb-server/src/main/java/com/cxy/server/service/impl/AdminServiceImpl.java文件

image.png

4.修改角色Mapper

修改:yeb/yeb-server/src/main/java/com/cxy/server/mapper/RoleMapper.java文件

image.png

5.修改角色xml

修改:yeb/yeb-server/src/main/resources/mapper/RoleMapper.xml文件

<?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="com.cxy.server.mapper.RoleMapper">

    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="com.cxy.server.pojo.Role">
        <id column="id" property="id"/>
        <result column="name" property="name"/>
        <result column="nameZh" property="nameZh"/>
    </resultMap>

    <!-- 通用查询结果列 -->
    <sql id="Base_Column_List">
        id, name, nameZh
    </sql>

    <!-- 根据用户 id 查询角色列表 -->
    <select id="getRoles" resultType="com.cxy.server.pojo.Role">
        SELECT
            r.id,
            r. NAME,
            r.nameZh
        FROM
            t_role AS r
        LEFT JOIN t_admin_role AS ar ON r.id = ar.rid
        WHERE
            ar.adminId = #{adminId}
    </select>
</mapper>
6.修改登录接口和Security配置文件

在登录和获取用户信息方法中添加getRoles()方法,登录和获取用户信息时能得到角色列表。
修改:yeb/yeb-server/src/main/java/com/cxy/server/controller/LoginController.java文件

image.png

修改:yeb/yeb-server/src/main/java/com/cxy/server/config/security/SecurityConfig.java文件
image.png

7.添加过滤器判断用户的角色

新建:yeb/yeb-server/src/main/java/com/cxy/server/config/security/component/CustomUrlDecisionManager.java文件

package com.cxy.server.config.security.component;

import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

/**
 * @author 陈鑫元
 * 权限控制
 * @description 判断用户角色
 * @date 2021-05-27 11:17
 * @since 1.0.0
 */
public class CustomUrlDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> ConfigAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        for (ConfigAttribute configAttribute : ConfigAttributes) {
            // 当前 url 所需角色
            String needRole = configAttribute.getAttribute();
            // 判断角色是否是登录即可访问的角色,此角色在在 CustomFilter 中设置
            if ("ROLE_LOGIN".equals(needRole)) {
                if (authentication instanceof AnonymousAuthenticationToken) {
                    throw new AccessDeniedException("尚未登录,请登录!");
                } else {
                    return;
                }
            }
            // 判断用户角色是否为 url 所需角色
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                if (authority.getAuthority().equals(needRole)) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("权限不足,请联系管理员!");
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return false;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return false;
    }
}
8.配置Security,添加动态权限控制

修改:yeb/yeb-server/src/main/java/com/cxy/server/config/security/SecurityConfig.java文件

image.png

(三)测试接口

修改:yeb/yeb-server/src/main/java/com/cxy/server/controller/HelloController.java文件,添加两个测试接口

package com.cxy.server.controller;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author 陈鑫元
 * @description 测试接口
 * @date 2021-05-23 17:22
 * @since 1.0.0
 */
@Api(tags = "HelloController")
@RestController
public class HelloController {
    @ApiOperation(value = "测试接口")
    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }

    @ApiOperation(value = "测试接口2")
    @GetMapping("/employee/basic/hello")
    public String hello2() {
        return "/employee/basic/hello";
    }

    @ApiOperation(value = "测试接口3")
    @GetMapping("/employee/advanced/hello")
    public String hello3() {
        return "/employee/advanced/hello";
    }
}

运行结果:

测试接口2:

image.png

测试接口3:

image.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容