SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题

因为篇幅原因请先参考这篇文章实现登录功能https://blog.csdn.net/grd_java/article/details/121925792
源码,码云 https://gitee.com/yin_zhipeng/spring-security-scaffolding

一、 数据库表,和实体类

因为前端是VUE写到,所以需要保存组件、路径等

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

二、菜单管理TODO

最终结果
在这里插入图片描述
在这里插入图片描述

1. 实体类

通过Mybatis-plus插件代码生成器,自动生成,然后添加一个字段children
在这里插入图片描述

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableField;
import java.io.Serializable;
import java.util.List;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

/**
 * <p>
 * 
 * </p>
 *
 * @author testjava
 * @since 2021-12-14
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value="DdMenu对象", description="")
public class DdMenu implements Serializable {

    private static final long serialVersionUID=1L;

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

    @ApiModelProperty(value = "父菜单id")
    private Integer pid;

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

    @ApiModelProperty(value = "菜单类型(目录,按钮)")
    private String type;

    @ApiModelProperty(value = "菜单权限")
    private String permission;

    @ApiModelProperty(value = "url")
    private String url;

    @ApiModelProperty(value = "path")
    private String path;

    @ApiModelProperty(value = "组件")
    private String component;

    @ApiModelProperty(value = "图标")
    @TableField("iconCls")
    private String iconCls;

    @ApiModelProperty(value = "是否保持激活")
    @TableField("keepAlive")
    private Boolean keepAlive;

    @ApiModelProperty(value = "是否要求权限")
    @TableField("requireAuth")
    private Boolean requireAuth;

    @ApiModelProperty(value = "是否启用")
    private Boolean enabled;

    @ApiModelProperty(value = "子菜单")
    @TableField(exist = false)//告诉Mybatis,表中没有这个字段,否则操作时会去数据库找这个字段
    private List<DdMenu> children;
}

2. Redis 缓存

  1. 启动redis
    在这里插入图片描述
  2. 添加依赖
    在这里插入图片描述
<!--redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--commons-pool2 对象池依赖-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>
  1. redis配置
    在这里插入图片描述
spring:
  redis: #redis配置
    host: 127.0.0.1 #你的redis地址
    port: 6379 #端口号
    database: 0
    timeout: 1800000
    lettuce:
      pool:
        max-active: 1024 # 最大连接数
        max-wait: -1 #最大阻塞等待时间(负数表示没限制)
        max-idle: 200 #最大空闲连接
        min-idle: 5 #最小空闲连接
  1. Redis配置类
    在这里插入图片描述
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;

/**
 * redis配置类
 */
@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        //String 类型 key序列器
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //String 类型 value序列器
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        //Hash 类型 key序列器
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        //Hash 类型 value序列器
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        //配置工厂
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        return redisTemplate;
    }
}

3. 编写逻辑,查询菜单

  1. controller
    在这里插入图片描述
import com.dd.security.entity.DdMenu;
import com.dd.security.service.DdMenuService;
import com.dd.security.service.DdUserService;
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 testjava
 * @since 2021-12-14
 */
@RestController
@RequestMapping("/security/dd-menu")
public class DdMenuController {
    @Autowired
    private DdMenuService ddMenuService;

    @ApiOperation(value = "通过用户id查询菜单列表")
    @GetMapping("/menu")
    public List<DdMenu> getMenusByUserId(){
        return ddMenuService.getMenusByUserId();
    }
}
  1. service
    在这里插入图片描述
import com.dd.security.entity.DdMenu;
import com.dd.security.entity.DdUser;
import com.dd.security.mapper.DdMenuMapper;
import com.dd.security.service.DdMenuService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
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.awt.*;
import java.util.List;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author testjava
 * @since 2021-12-14
 */
@Service
public class DdMenuServiceImpl extends ServiceImpl<DdMenuMapper, DdMenu> implements DdMenuService {

    @Autowired
    private DdMenuMapper ddMenuMapper;

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 根据用户id获取菜单
     */
    @Override
    public List<DdMenu> getMenusByUserId() {
        //Security全局上下文获取UserDetails对象
        DdUser user = (DdUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        //获取redis对象
        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
        //先从redis中查询
        List<DdMenu> menus = (List<DdMenu>) valueOperations.get("menu_" + user.getId());
        //如果redis中没有,查询mysql数据库
        if(CollectionUtils.isEmpty(menus)){
            menus = ddMenuMapper.getMenusByUserId(user.getId());
            //将查询出来的内容放在reids中
            valueOperations.set("menu_" + user.getId(),menus);
        }
        //返回结果
        return menus;
    }
}

  1. mapper
    在这里插入图片描述
select
	distinct
	m1.*,
	m2.id as id2,
	m2.pid as pid2,
	m2.`name` as name2,
	m2.type as type2,
	m2.permission as permission2,
	m2.url as url2,
	m2.path as path2,
	m2.component as component2,
	m2.iconCls as iconCls2,
	m2.keepAlive as keepAlive2,
	m2.requireAuth as requireAuth,
	m2.enabled as enabled2
from
	dd_menu as m1 
inner join
	dd_menu as m2 #自关联
on
	m1.id = m2.pid
inner join
	dd_role_menu as rm
on
	rm.mid = m2.id
inner join
	dd_user_role as ur
on
	ur.rid = rm.rid
where
	ur.uid = 1

在这里插入图片描述
在这里插入图片描述

<?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.dd.security.mapper.DdMenuMapper">

    <!-- DdMenu映射对象-->
    <resultMap id="BaseResultMap" type="com.dd.security.entity.DdMenu">
        <id column="id" property="id" />
        <result column="pid" property="pid" />
        <result column="name" property="name" />
        <result column="type" property="type" />
        <result column="permission" property="permission" />
        <result column="url" property="url" />
        <result column="path" property="path" />
        <result column="component" property="component" />
        <result column="iconCls" property="iconCls" />
        <result column="keepAlive" property="keepAlive" />
        <result column="requireAuth" property="requireAuth" />
        <result column="enabled" property="enabled" />
    </resultMap>
    <!--子菜单,继承上面DdMenu映射对象-->
    <resultMap id="Menus" type="com.dd.security.entity.DdMenu" extends="BaseResultMap">
        <collection property="children" ofType="com.dd.security.entity.DdMenu">
            <id column="id2" property="id" />
            <result column="pid2" property="pid" />
            <result column="name2" property="name" />
            <result column="type2" property="type" />
            <result column="permission2" property="permission" />
            <result column="url2" property="url" />
            <result column="path2" property="path" />
            <result column="component2" property="component" />
            <result column="iconCls2" property="iconCls" />
            <result column="keepAlive2" property="keepAlive" />
            <result column="requireAuth2" property="requireAuth" />
            <result column="enabled2" property="enabled" />
        </collection>
    </resultMap>

    <select id="getMenusByUserId" resultMap="Menus">
        select
            distinct
            m1.*,
            m2.id as id2,
            m2.pid as pid2,
            m2.`name` as name2,
            m2.type as type2,
            m2.permission as permission2,
            m2.url as url2,
            m2.path as path2,
            m2.component as component2,
            m2.iconCls as iconCls2,
            m2.keepAlive as keepAlive2,
            m2.requireAuth as requireAuth,
            m2.enabled as enabled2
        from
            dd_menu as m1
                inner join
            dd_menu as m2
            on
                m1.id = m2.pid
                inner join
            dd_role_menu as rm
            on
                rm.mid = m2.id
                inner join
            dd_user_role as ur
            on
                ur.rid = rm.rid
        where
            ur.uid = #{id}
    </select>
</mapper>

三、url角色权限

我们如果只控制菜单,用户登录后进入管理系统,只能看到自己角色权限对应的菜单,但是如果这个用户此时通过某种手段获取了我们后台的接口对应的url,那么他就算前端看不到相应的菜单和按钮,无法通过事件访问后端接口。也可以直接通过浏览器地址栏输入url,因为它已经登录过,这时,就可以直接访问url了,尽管我们不想让他具有访问这些接口的权限,让他看不见相应的菜单和按钮,此时也只能眼睁睁看着他获取数据
如何处理这种情况呢,我们的菜单表有一个url字段,记录了url地址
  1. 我们在菜单实体类中,添加访问此url和菜单,需要的角色、权限属性
  2. 规定,菜单表中的url都需要特定的角色和权限才能访问,取余的url,可以允许没有角色或拥有LOGIN_ROLE默认角色的用户访问。
  3. 当用户访问后端时,先过滤请求,判断用户请求的是否是菜单表的url,如果是,判断是否具有相应权限,没有权限拦截请求,返回权限不足。如果不是菜单表url,为其添加默认角色LOGIN_ROLE角色
最终效果

在这里插入图片描述
在这里插入图片描述
访问需要ROLE_admin角色的路径可以正常访问
在这里插入图片描述
访问需要其它角色的url提示权限不足
在这里插入图片描述
访问不需要授权的路径,不会触发过滤,不会拦截判断url需要的权限
在这里插入图片描述

1. url权限,在全局中设置用户访问的url,需要哪些权限

1. 实现获取每个菜单所需角色权限

实现根据角色获取菜单列表,就是每个菜单有哪些角色有权限

在这里插入图片描述

  1. 修改菜单实体类,添加角色属性
    在这里插入图片描述
  2. service接口
    在这里插入图片描述
    在这里插入图片描述
  3. mapper
    在这里插入图片描述
    在这里插入图片描述
<!--子菜单,继承上面DdMenu映射对象-->
<resultMap id="MenusWithRoler" type="com.dd.security.entity.DdMenu" extends="BaseResultMap">
    <collection property="roles" ofType="com.dd.security.entity.DdRole">
        <id column="rid" property="id" />
        <result column="rname" property="name" />
        <result column="rnameZh" property="nameZH" />
    </collection>
</resultMap>
<select id="getMenusWithRole" resultMap="MenusWithRoler">
    select
        m.*,
        r.id as rid,
        r.`name` as rname,
        r.name_zh as rnameZh
    from
        dd_menu as m
    inner join
        dd_role_menu as rm
    on
        m.id = rm.mid
    inner join
        dd_role as r
    on
        r.id = rm.rid
    order by m.id
</select>

2. SpringSecurity 过滤器,拦截请求,获取url,判断url需要哪些角色权限

截图中忘了加@Component注解
在这里插入图片描述

import com.dd.security.entity.DdMenu;
import com.dd.security.entity.DdRole;
import com.dd.security.service.DdMenuService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.util.AntPathMatcher;

import java.util.Collection;
import java.util.List;

/**
 * 权限控制
 * 根据请求url分析所需角色
 */
 @Component
public class CustomFilter implements FilterInvocationSecurityMetadataSource {
    @Autowired
    private DdMenuService ddMenuService;


    private AntPathMatcher antPathMatcher = new AntPathMatcher();
    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        //获取FilterInvocation对象
        FilterInvocation filterInvocation = (FilterInvocation) o;
        //获取请求url
        String requestUrl = filterInvocation.getRequestUrl();
        //获取到每个菜单的对应角色,就是这个url,哪些角色有权限
        List<DdMenu> menusWithRole = ddMenuService.getMenusWithRole();
        //遍历菜单
        for (DdMenu m : menusWithRole){
            //如果用户请求url和菜单中url匹配
            if(antPathMatcher.match(m.getUrl(),requestUrl)){
                //流式编程+lambda表达式:类名::方法名 方法引用,DdRole::getName 表示引用DdRole类的getName方法
                //类名::new 构造方法引用,String[]::new 表示引用String[]的构造方法,构造一个数组
                //获取角色列表,也就是说,用户请求的url,需要具备以下角色
                String[] roles = m.getRoles().stream().map(DdRole::getName).toArray(String[]::new);

                //org.springframework.security.access.SecurityConfig;
                //将我们角色放到Security中
                return SecurityConfig.createList(roles);
            }
        }
        //如果用户请求url不是菜单中url,给予默认角色ROLE_LOGIN(登录角色),也就是必须拥有登录角色才能访问
        return SecurityConfig.createList("ROLE_LOGIN");
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return false;
    }
}

2. 获取用户角色,判断用户角色权限能否访问url

1. 实现获取角色

  1. 实体类
    在这里插入图片描述
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
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.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value="DdUser对象,使用Spring Security框架就要继承UserDetails接口,实现方法,将返回值改为true", description="")
public class DdUser implements UserDetails {
    private static final long serialVersionUID=1L;

    @ApiModelProperty(value = "id")
    @TableId(value = "id", type = IdType.ID_WORKER_STR)
    private Integer id;
    private String username;
    private String password;

    @ApiModelProperty(value = "用户角色")
    @TableField(exist = false)
    private List<DdRole> roles;
    /**
     * 所有权限
     * @return
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {

        List<SimpleGrantedAuthority> authorities =
                roles.stream()
                        //将每一个角色,遍历成Security指定的权限字符对象,
                        // 比如ROLE_admin要封装成new SimpleGrantedAuthority("ROLE_admin")
                .map(role -> new SimpleGrantedAuthority(role.getName()))
                .collect(Collectors.toList());//然后返回为list
        return authorities;//将角色权限返回
    }

    /**
     * 账号是否过期
     * @return
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 账号是否被锁定
     * @return
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * 凭证(密码)是否过期
     * @return
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 是否启用
     * @return
     */
    @Override
    public boolean isEnabled() {
        return true;
    }
}
  1. service
    在这里插入图片描述
    在这里插入图片描述
  2. mapper
    在这里插入图片描述
    在这里插入图片描述在这里插入图片描述
<!--/**
 * 根据用户id获取用户拥有角色
 */-->
<select id="getRolesByUserId" resultType="com.dd.security.entity.DdRole" parameterType="java.lang.Integer">
    select
        r.id as id,
        r.name as name,
        r.name_zh as nameZh
    from
        dd_role as r
            inner join
        dd_user_role as ur
        on
            r.id = ur.rid
    where
        ur.uid = #{userId}
</select>
  1. 修改获取用户信息
    在这里插入图片描述
  2. 修改自定义UserDetailsService
    在这里插入图片描述
 /**
  * 配置UserDetailsService
  */
 @Bean
 @Override
 public UserDetailsService userDetailsService() {
     return username->{
         DdUser user = ddUserService.getLoginInfoByUsername(username);
         if(user == null){
             throw new UsernameNotFoundException("用户名或密码不正确");
         }
         List<DdRole> rolesByUserId = ddRoleService.getRolesByUserId(user.getId());
         user.setRoles(rolesByUserId);
         return user;
     };
 }

2. 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;
@Component
public class CustomUrlDecisionManager implements AccessDecisionManager {
    /**
     *
     * @param authentication 当前访问用户
     * @param o
     * @param collection Collection<ConfigAttribute> 我们在上一个过滤器CustomFilter implements FilterInvocationSecurityMetadataSource中配置了
     * @throws AccessDeniedException
     * @throws InsufficientAuthenticationException
     */
    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {

        //遍历ConfigAttribute
        for(ConfigAttribute configAttribute : collection){
            //获取当前访问url,需要的角色权限,这些值在再CustomFilter中设置进去的
            String needRole = configAttribute.getAttribute();
            //判断url是否登录即可访问,再CustomFilter 中设置
            if("ROLE_LOGIN".equals(needRole)){
                //如果当前用户是匿名用户(未登录用户),抛异常,让用户登录
                if(authentication instanceof AnonymousAuthenticationToken){
                    throw new AccessDeniedException("尚未登录,请登录!!!");
                }else{
                    return;
                }
            }
            //如果当前url,不是登录就可以访问的
            //判断当前用户的GrantedAuthority里面有没有需要的角色,如果有就放行
            //如果用户没有相应角色,就不放行,同时:没登录的用户因为没有角色,也不会被放行
            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;
    }
}

3. 配置SpringSecurity,让过滤器生效

说明:这些过滤器不会过滤在Security配置类中放行的路径

在这里插入图片描述

  1. 引入两个过滤器
    在这里插入图片描述
  2. 动态权限配置两个过滤器
    在这里插入图片描述
@Override
protected void configure(HttpSecurity http) throws Exception {

    //使用JWT不需要csrg
    http.csrf().disable()
            //使用Token,不需要session
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()//接下来配置授权
            .authorizeRequests()
            //下面授权在configure(WebSecurity web)方法中配置了,这里不需要了
//                .antMatchers("/login","/logout").permitAll()//允许访问/login,/logout的请求无需认证即可通行
            .anyRequest().authenticated()//除了上面配置的,剩下的请求全部拦截,必须认证通过才能访问
            .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {//动态权限配置
                @Override
                public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                    o.setAccessDecisionManager(customUrlDecisionManager);//判断用户角色是否可以访问url过滤器
                    o.setSecurityMetadataSource(customFilter);//根据请求url分析所需角色过滤器
                    return o;
                }
            })
            .and()//接下来配置缓存
            .headers()
            .cacheControl()
            ;
    //添加JWT登录授权过滤拦截器
    http.addFilterBefore(jwtAuthencationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
    //添加自定义未授权,和未登录结果返回,前后端分离,需要返回状态码
    http.exceptionHandling()
            .accessDeniedHandler(restAccessDeniedHandler)
            .authenticationEntryPoint(restAuthorizationEntryPoint);

}

4. 测试

在这里插入图片描述
在这里插入图片描述

四、权限组功能实现

1. 角色

SpringSecurity 的角色必须带有ROLE_前缀,否则不会被SpringSecurity捕获

正确的角色名:ROLE_admin、ROLE_adsfasdf
错误的角色名:r_admin、admin、ROLE_

  1. 所以当我们添加角色时,要判断用户是否以ROLE_开头,不是就补充上再执行添加逻辑,是就直接执行添加逻辑
  1. Controller
    在这里插入图片描述
import com.dd.common_utils.Result;
import com.dd.security.entity.DdRole;
import com.dd.security.service.DdRoleService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
@RequestMapping("/security/dd-role")
public class DdRoleController {
    @Autowired
    private DdRoleService ddRoleService;

    @ApiOperation("获取所有角色")
    @GetMapping("/")
    public Result getAllRoles(){
        List<DdRole> list = ddRoleService.list();
        if(list.isEmpty()){
            return Result.error().message("没有获取的任何角色信息!!!");
        }
        return Result.ok().data("RoleAllList",list);
    }
    @ApiOperation("添加角色")
    @PostMapping("/")
    public Result addRole(@RequestBody DdRole ddRole){
        //如果角色名不是ROLE_打头,就补充上
        if(!ddRole.getName().startsWith("ROLE_")){
            ddRole.setName("ROLE_"+ddRole.getName());
        }
        if(ddRoleService.save(ddRole)){
            return Result.ok().message("添加成功");
        }
        return Result.error().message("添加失败");
    }

    @ApiOperation("删除角色")
    @DeleteMapping("/role/{id}")
    public Result deleteRole(@PathVariable(value = "id",name = "id") Integer id){
        if(ddRoleService.removeById(id)){
            return Result.ok().message("删除成功");
        }
        return Result.error().message("删除失败");
    }
}

2. 菜单

  1. controller
    在这里插入图片描述
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.dd.common_utils.Result;
import com.dd.security.entity.DdMenu;
import com.dd.security.entity.DdRoleMenu;
import com.dd.security.service.DdMenuService;
import com.dd.security.service.DdRoleMenuService;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @author testjava
 * @since 2021-12-14
 */
@RestController
@RequestMapping("/security/dd-menu")
public class DdMenuController {
    @Autowired
    private DdMenuService ddMenuService;
    @Autowired
    private DdRoleMenuService ddRoleMenuService;

    @ApiOperation(value = "通过用户id查询菜单列表")
    @GetMapping("/menu")
    public List<DdMenu> getMenusByUserId(){
        return ddMenuService.getMenusByUserId();
    }

    @ApiOperation(value = "查询所有菜单")
    @GetMapping("/menus")
    public Result getMenus(){
        List<DdMenu> list = ddMenuService.list();
        if(list.isEmpty()){
            return Result.error().message("没有菜单!!");
        }
        return Result.ok().data("menuAllList",list);
    }

    @ApiOperation(value = "根据角色id查询菜单id")
    @GetMapping("/menuIdByRoleId/{rid}")
    public Result getMenuIdByRoleId(@PathVariable Integer rid){
        List<Integer> mids = ddRoleMenuService.list(new QueryWrapper<DdRoleMenu>().eq("rid", rid))
                .stream().map(DdRoleMenu::getMid).collect(Collectors.toList());
        if(mids.isEmpty()){
            return Result.error().message("获取菜单失败");
        }
        return Result.ok().data("menuId",mids);
    }

    @ApiOperation(value = "根据角色id查询菜单")
    @GetMapping("/menuByRoleId/{rid}")
    public Result getMenuByRoleId(@PathVariable Integer rid){
        List<DdMenu> list = ddMenuService.getMenuByRoleId(rid);
        if(list.isEmpty()){
            return Result.error().message("没有菜单!!");
        }
        return Result.ok().data("menuList",list);
    }

    @ApiOperation(value = "更新角色菜单")
    @PutMapping("/")
    public Result updateMenuRole(Integer rid,Integer[] mids){
        return ddMenuService.updateMenuRole(rid,mids);
    }
}
  1. service
    在这里插入图片描述
    在这里插入图片描述
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.dd.common_utils.Result;
import com.dd.security.entity.DdMenu;
import com.dd.security.entity.DdRoleMenu;
import com.dd.security.entity.DdUser;
import com.dd.security.mapper.DdMenuMapper;
import com.dd.security.mapper.DdRoleMenuMapper;
import com.dd.security.service.DdMenuService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.dd.security.service.DdRoleMenuService;
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.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import java.awt.*;
import java.util.List;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author testjava
 * @since 2021-12-14
 */
@Service
public class DdMenuServiceImpl extends ServiceImpl<DdMenuMapper, DdMenu> implements DdMenuService {

    @Autowired
    private DdMenuMapper ddMenuMapper;
    @Autowired
    private DdRoleMenuMapper ddRoleMenuMapper;

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 根据用户id获取菜单
     */
    @Override
    public List<DdMenu> getMenusByUserId() {
        //Security全局上下文获取UserDetails对象
        DdUser user = (DdUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        //获取redis对象
        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
        //先从redis中查询
        List<DdMenu> menus = (List<DdMenu>) valueOperations.get("menu_" + user.getId());
        //如果redis中没有,查询mysql数据库
        if(CollectionUtils.isEmpty(menus)){
            menus = ddMenuMapper.getMenusByUserId(user.getId());
            //将查询出来的内容放在reids中
            valueOperations.set("menu_" + user.getId(),menus);
        }
        //返回结果
        return menus;
    }

    /**
     * 根据角色获取菜单列表
     */
    @Override
    public List<DdMenu> getMenusWithRole() {
        return ddMenuMapper.getMenusWithRole();
    }

    @Override
    public List<DdMenu> getMenuByRoleId(Integer rid) {
        return ddMenuMapper.getMenuByRoleId(rid);
    }

    /**
     * 更新角色菜单
     * 先将当前角色所有菜单都删除,然后再更新
     * @param rid 角色id
     * @param mids 要更新的菜单id
     */
    @Override
    @Transactional//自己写更新的操作,一定要加事务
    public Result updateMenuRole(Integer rid, Integer[] mids) {
        //先全删了,否则得一次次判断,太费资源
        ddRoleMenuMapper.delete(new QueryWrapper<DdRoleMenu>().eq("rid", rid));
        //要更新的菜单id为空,则直接返回
        if(null == mids||mids.length == 0){
            return Result.ok().message("更新菜单成功");
        }
        //如果传了菜单id过来,则更新
        ddMenuMapper.insertRecord(rid,mids);
        return null;
    }
}
  1. mapper
    在这里插入图片描述
    在这里插入图片描述
    <!--更新角色菜单-->
    <insert id="insertRecord">
        insert into dd_role_menu(rid,mid) values
        <foreach collection="mids" item="mid" separator=",">
            (#{rid},#{mid})
        </foreach>
    </insert>

3. 用户

  1. controller
    在这里插入图片描述
    在这里插入图片描述
import com.dd.common_utils.Result;
import com.dd.security.entity.DdMenu;
import com.dd.security.entity.DdUser;
import com.dd.security.service.DdUserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.parameters.P;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @author testjava
 * @since 2021-12-13
 */
@RestController
@Api("传递查询条件时,不要携带security的字段,只携带DdUser对象本身的字段,JSON")
@RequestMapping("/security/dd-user-role")
public class DdUserRoleController {
    @Autowired
    private DdUserService ddUserService;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @ApiOperation("获取所有操作员以及它拥有的角色和权限,传递查询条件时,不要携带security的字段,只携带DdUser对象本身的字段,JSON")
    @PostMapping("/")
    public Result getAllUser(@RequestBody DdUser ddUser){
        List<DdUser> list = ddUserService.getAllUser(ddUser);
        if(list.isEmpty()){
           return Result.error().message("没有获取的用户信息!!!");
        }
        return Result.ok().data("allUserList",list);
    }

    @ApiOperation("更新用户")
    @PutMapping("/")
    public Result updateUser(@RequestBody DdUser ddUser){
        ddUser.setPassword(null);//不允许直接改密码

        if(ddUserService.updateById(ddUser)){
          return Result.ok().message("修改用户成功!!!");
        }
        return Result.error().message("修改失败!!!");
    }

    @ApiOperation("更新用户拥有的角色")
    @PutMapping("/updateUserRole")
    public Result updateUserRole(Integer uid,Integer[] rids){
        return ddUserService.updateUserRole(uid,rids);
    }

    @ApiOperation("修改密码")
    @PutMapping("/updatePasswordById/{id}/{password}")
    public Result updatePassword(@PathVariable Integer id, @PathVariable String password){
        String encode = passwordEncoder.encode(password);
        DdUser ddUser = new DdUser();
        ddUser.setId(id);
        ddUser.setPassword(encode);
        if(ddUserService.updateById(ddUser)){
           return Result.ok().message("修改密码成功!!!");
        }
        return Result.error().message("修改密码失败!!!");
    }

    @ApiOperation("删除用户")
    @DeleteMapping("/{id}")
    public Result deleteById(@PathVariable Integer id){
        if(ddUserService.removeById(id)){
            return Result.ok().message("删除成功!!!");
        }
        return Result.error().message("删除失败!!!");
    }

}
  1. service
    在这里插入图片描述
    在这里插入图片描述
@Autowired
private DdUserRoleMapper ddUserRoleMapper;
/**
 * 获取所有操作员
 */
@Override
public List<DdUser> getAllUser(DdUser ddUser) {

    return ddUserMapper.getAllUser(ddUser);
}

/**
 * 更新用户拥有的角色
 * @param uid 用户id
 * @param rids 用户要修改的角色id
 */
@Override
@Transactional//自己写更新的操作,一定要加事务
public Result updateUserRole(Integer uid, Integer[] rids) {
    //先全删了,否则得一次次判断,太费资源
    ddUserRoleMapper.delete(new QueryWrapper<DdUserRole>().eq("uid", uid));
    //要更新的菜单id为空,则直接返回
    if(null == rids||rids.length == 0){
        return Result.ok().message("更新用户角色成功");
    }
    //如果传了菜单id过来,则更新
    ddUserMapper.insertRecord(uid,rids);
    return Result.ok().message("更新菜单成功");
}
  1. 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="com.dd.security.mapper.DdUserMapper">

    <!--结果集封装-->
    <resultMap type="com.dd.security.entity.DdUser" id="DdUserResult">
        <result property="id"    column="id"    />
        <result property="username"    column="username"    />
        <result property="password"    column="password"    />
    </resultMap>

    <resultMap id="DdUserRoleResult" type="com.dd.security.entity.DdUser" extends="DdUserResult">
        <collection property="roles" ofType="com.dd.security.entity.DdRole">
            <id column="rid" property="id" />
            <result column="rname" property="name" />
            <result column="rnameZh" property="nameZh" />
        </collection>
    </resultMap>

    <select id="selectByUsername" parameterType="java.lang.String" resultType="com.dd.security.entity.DdUser">
        select id,username,password from dd_user where username=#{username}
    </select>

    <select id="getAllUser" resultMap="DdUserRoleResult" parameterType="com.dd.security.entity.DdUser">
        select
        u.id as id,
            u.username as username,
            u.`password` as `password`,
            r.id as rid,
            r.`name` as rname,
            r.name_zh as rnameZh
        from
            dd_user as u
        inner join
            dd_user_role as ur
        on
            u.id = ur.uid
        inner join
            dd_role as r
        on
            ur.rid = r.id
        <where>
            <trim>
                <if test="id != null  and id != '' and id!=0">and u.id = #{id}</if>
                <if test="username != null  and username != ''">and u.username like concat('%', #{username}, '%')</if>
                <if test="roles != null">and r.name_zh like concat('%', #{roles[0].nameZh}, '%')</if>
            </trim>
        </where>
    </select>
    <!--更新用户角色-->
    <insert id="insertRecord">
        insert into dd_user_role(uid,rid) values
        <foreach collection="rids" item="rid" separator=",">
            (#{uid},#{rid})
        </foreach>
    </insert>
</mapper>

4. 主要功能测试

  1. 菜单
    在这里插入图片描述
    在这里插入图片描述

五、 使用redis存储验证码

存储

在这里插入图片描述

        //获取用户ip作为键值
        String remoteAddr = request.getRemoteAddr();
        //获取redis对象
        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
        //设置验证码到redis,时限60秒,key为ip地址+"captcha"
        valueOperations.set(remoteAddr+"captcha",code,60, TimeUnit.SECONDS);
登录时获取

在这里插入图片描述

        ValueOperations<String,Object> valueOperations = redisTemplate.opsForValue();
        String remoteAddr = request.getRemoteAddr();
        String captcha = (String)valueOperations.get(remoteAddr + "captcha");

六、使用分页查询和添加操作

一些小伙伴私信我,没做分页,没做添加,我这里提供整合分页的方法,大家自己写个controller接口,照猫画虎吧

1. 分页

  1. 集成PageHelper插件
  1. 引入依赖
    在这里插入图片描述
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.4.1</version>
        </dependency>
  1. yaml配置文件配置插件
    在这里插入图片描述
# PageHelper分页插件
pagehelper:
  helperDialect: mysql
  reasonable: true
  supportMethodsArguments: true
  params: count=countSql
接下来一点很重要,保证你的实体类中,没有直接实例化的容器,否则当容器没有内容时,会报空指针异常
  1. 下面roles是一个集合,并在实例化时,进行集合注入,此时一旦roles为null,pageHeader依然执行方法,进行对集合的操作,此时就会报空指针异常。所以我们加if判断
    在这里插入图片描述
  2. 另外,这个玩意遍历拼接的是role的name属性,所以最好对name也做判断
    在这里插入图片描述
整合完成以后,我们就可以使用了,接下来提供新接口使用分页插件实现分页

在这里插入图片描述

    @ApiOperation("分页获取所有操作员以及它拥有的角色和权限,传递查询条件时,不要携带security的字段,只携带DdUser对象本身的字段,JSON")
    @PostMapping("/page/{current}/{size}")
    public Result getAllUser(@RequestBody DdUser ddUser,@PathVariable Integer current,@PathVariable Integer size){
        Page<Object> objects = PageHelper.startPage(current, size);

        List<DdUser> list = ddUserService.getAllUser(ddUser);
        if(list.isEmpty()){
            return Result.error().message("没有获取的用户信息!!!");
        }
        System.out.println("当前页码(从1开始的)"+objects.getPageNum());
        System.out.println("总页数"+objects.getPages());
        System.out.println("共有多少条数据"+objects.getTotal());
        return Result.ok()
                .data("allUserListPage",objects)
                .data("pageNum",objects.getPageNum())
                .data("pages",objects.getPages())
                .data("total",objects.getTotal());
    }

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

现在,只有你想分页,就按照上面的步骤,提供新接口,然后在要执行sql的地方上面,添加代码PageHelper.startPage(current,size);即可
  1. 注意,它只对此行代码下面的第一条执行的sql进行分页

2. 添加操作

  1. 我们要生成随机id,可以借助一个工具hutool-all,我们引入maven依赖
    在这里插入图片描述
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.20</version>
        </dependency>
  1. 为了方便我将,我将数据库和java实体类的id都换成了字符串型,你可能需要改一些地方,mapper.xml文件中也可能需要修改
    在这里插入图片描述
  2. controller,使用工具类生成随机id,注意密码需要加密
    在这里插入图片描述
    @ApiOperation("添加用户")
    @PostMapping("/add")
    public Result addUser (@RequestBody DdUser ddUser){
        Snowflake snowflake = IdUtil.getSnowflake(0,0);
        ddUser.setId(snowflake.nextIdStr());//生成一个随机id
        //密码需要加密
        String encode = passwordEncoder.encode(ddUser.getPassword());
        ddUser.setPassword(encode);
        if(ddUserService.save(ddUser)){
            return Result.ok().message("添加成功");
        }
        return Result.error().message("添加失败");
    }
  1. 测试
    在这里插入图片描述

七、解决高版本循环依赖问题

实现了权限管理系统后,出现高版本循环依赖的问题,如下
  1. 当spring boot版本为2.6.X以上时,出现问题
    在这里插入图片描述
    在这里插入图片描述
原因如下
  1. SecurityConfig配置类中,注入了private DdUserService ddUserService;
    在这里插入图片描述
  2. DdUserServiceImpl又注入了UserDetailsService
    在这里插入图片描述
解决方案
  1. SecurityConfig注释private DdUserService ddUserService;,然后使用DdUserMapper
    在这里插入图片描述
    在这里插入图片描述
循环依赖解决,但是又出现了最近经常出现的问题,springboot2.6.x与swagger2不兼容的问题,这个和咱代码就没关系了,解决方案如下,修改配置,或者升级swagger或者降低springboot版本到2.5.x
  1. 解决循环依赖,新问题如下,表面上是swagger2找不到东西了,其实是ant_path_matcher参数的问题
    在这里插入图片描述
  2. 配置yaml参数
    在这里插入图片描述
spring:
  main:
    allow-circular-references: true # 关闭spring boot 循环注入检测
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher # 解决spring boot 高版本不兼容swagger2.x版本问题
  1. 问题解决,项目跑起来了,但是此时swagger不能用了,显示跨域问题
    在这里插入图片描述
  2. 修改跨域配置
    在这里插入图片描述
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
//                .allowedOrigins("*")
                .allowedOriginPatterns("*")
                .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
                .allowCredentials(true)
                .maxAge(3600)
                .allowedHeaders("*");
    }

八、前端对接

由于篇幅限制,我将其放在这篇文章中:https://blog.csdn.net/grd_java/article/details/122770777
  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

殷丿grd_志鹏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值