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

一.验证码功能实现

(一)使用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;

/**
 * 

* 服务实现类 *

* * @author 陈鑫元 * @since 2021-05-21 */ @Service public class AdminServiceImpl extends ServiceImpl 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 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().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;

/**
 * 

* 前端控制器 *

* * @author 陈鑫元 * @since 2021-05-21 */ @RestController @RequestMapping("/system/cfg") public class MenuController { @Autowired private IMenuService menuService; @ApiOperation(value = "通过用户id查询菜单列表") @GetMapping("/menu") public List
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;

/**
 * 

* 服务类 *

* * @author 陈鑫元 * @since 2021-05-21 */ public interface IMenuService extends IService
{ /** * 通过用户id查询菜单列表 * * @return */ List 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;

/**
 * 

* 服务实现类 *

* * @author 陈鑫元 * @since 2021-05-21 */ @Service public class MenuServiceImpl extends ServiceImpl implements IMenuService { @Autowired private MenuMapper menuMapper; /** * 通过用户id查询菜单列表 * * @return */ @Override public List
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;

/**
 * 

* Mapper 接口 *

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

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





    
    
        
        
        
        
        
        
        
        
        
        
    

    
        
            
            
            
            
            
            
            
            
            
            
        
    

    
    
        id, url, path, component, name, iconCls, keepAlive, requireAuth, parentId, enabled
    

    
    


(二)测试接口

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 redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate 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;

/**
 * 

* 服务实现类 *

* * @author 陈鑫元 * @since 2021-05-21 */ @Service public class MenuServiceImpl extends ServiceImpl implements IMenuService { @Autowired private MenuMapper menuMapper; @Autowired private RedisTemplate redisTemplate; /** * 通过用户id查询菜单列表 * * @return */ @Override public List
getMenusByAdminId() { Integer adminId = ((Admin) SecurityContextHolder .getContext().getAuthentication().getPrincipal()).getId(); ValueOperations valueOperations = redisTemplate.opsForValue(); // 从 redis 获取菜单数据 @SuppressWarnings("unchecked") List menus = (List) 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文件





    
    
        
        
        
        
        
        
        
        
        
        
    

    
        
            
            
            
            
            
            
            
            
            
            
        
    
    
        
            
            
            
        
    

    
    
        id, url, path, component, name, iconCls, keepAlive, requireAuth, parentId, enabled
    

    
    

    
    

(二)判断用户登录角色

判断登录的用户都有那些角色,并和跟据请求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;

/**
 * 

* 管理员表 *

* * @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 roles; @Override public Collection getAuthorities() { List 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文件





    
    
        
        
        
    

    
    
        id, name, nameZh
    

    
    

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 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 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

你可能感兴趣的:(第八天:验证码、菜单、角色功能实现)