背景:菜单和权限在系统中是非常重要的事情,在结合自己研究过的Spring security和项目前后端实践中对进行总结。

介绍:使用基于RBAC权限模型,针对角色分配不同的权限

java spring 菜单权限管理 springboot 菜单权限_用户信息

数据库设计:

系统菜单                                                系统角色                                              菜单角色表

 

java spring 菜单权限管理 springboot 菜单权限_java spring 菜单权限管理_02

      

java spring 菜单权限管理 springboot 菜单权限_ide_03

           

java spring 菜单权限管理 springboot 菜单权限_用户信息_04

             

  用户对应的角色                               用户信息

 

java spring 菜单权限管理 springboot 菜单权限_用户信息_05

                

java spring 菜单权限管理 springboot 菜单权限_ide_06

技术:Spring security+jjwt

Spring security:是Spring 开源的权限管理框架,由一组过滤器链组成,对不同的访问进去拦截和控制,也可以自己实现权限拦截

spring security 的核心功能主要包括:

  • 认证 (你是谁)
  • 授权 (你能干什么)
  • 攻击防护 (防止伪造身份)

jjwt:是一个提供端到端的JWT创建和验证的Java库,可以生成加密的token,并可以从token反推出存放在token的一些信息(如用户账号)——参考官网https://jwt.io/introduction/

 

实现:通过UserDetailsService 和UserDetails 通过数据库获取用户信息如(权限,用户账号)

步骤一:

// 定义jjwt的用户的一些信息,在后面生成token时需要,并且Spring security要获取实现UserDetails 接口用户信息  
@Getter
@AllArgsConstructor
public class SystemUser implements UserDetails {

    @JSONField(serialize = false)
    private final Long id;

    private final String username;

    @JSONField(serialize = false)
    private final String password;

    public Long getId() {
        return id;
    }

    private final String salt;

// 权限
    @JSONField(serialize = false)
    private final Collection<GrantedAuthority> authorities;


    @JSONField(serialize = false)
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @JSONField(serialize = false)
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @JSONField(serialize = false)
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return false;
    }

    @JSONField(serialize = false)
    @Override
    public String getPassword() {
        return password;
    }


    public Collection getRoles() {
        return authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet());
    }
}

 

步骤二:实现 UserDetailsService 接口,这里我使用mybatis查询数据库,通过用户账号获取数据库用户信息

public class SystemUserDetailsService implements UserDetailsService {

    @Autowired
    private ISysUserService userService;

    @Autowired
    private JwtPermissionService permissionService; // 获取用户角色的菜单权限

    @Override
    public UserDetails loadUserByUsername(String username) {
        SysUser user = userService.findByName(username);
        if (user == null) {
            throw new ServiceException("账号不存在");
        } else {
            if (user.getUserStatus().equals(Constants.OrganizationStatus.DISABLE)) {
                throw new ServiceException("账号已被禁用");
            }
            return createJwtUser(user);
        }
    }

    public UserDetails createJwtUser(SysUser user) {
        return new SystemUser(
                user.getId(),
                user.getUsername(),
                user.getPassword(),
                user.getSalt(),
                permissionService.mapToGrantedAuthorities(user),
                user.getCreateTime()
        );
    }
}

  

步骤三:JwtPermissionService 实现,请注意这是实现的虚假逻辑,具体的还要看业务逻辑

@Component
public class JwtPermissionService{

@Autowired
private IUsersRolesService usersRolesService;
@Autowired
private IRolesMenuService rolesMenuService;
public Collection<GrantedAuthority> mapToGrantedAuthorities(SysUser user){
// step 1 根据用户账号获取用户的角色
  Set<Role> menu =usersRolesService.getRole(String userName);
// step 2 根据角色获取用户的菜单
Set<Menu>menuList=rolesMenuService.getMenu(Set<Role>role);
// step 3 获取菜单对应的menu_make 进行转换

return menuVos.stream().filter(x -> !StringUtils.isEmpty(x.getMenuMark())).map(result -> {

String permission =result.getMenuMark();
return new SimpleGrantedAuthority(permission);}
).collect(Collectors.toList());
}
}

 

 步骤4:定义Spring security  权限配置

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtTokenFilter  tokenFilter;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    auth
            .userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoderBean());
}
@Bean
GrantedAuthorityDefaults grantedAuthorityDefaults() {
    // 去掉前缀
    return new GrantedAuthorityDefaults("");
}

// 加密方式
@Bean
public PasswordEncoder passwordEncoderBean() {
    return new BCryptPasswordEncoder();
}
@Bean
@Override 
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}// 权限拦截规则,千万不要.login() 这直接走表单验证了,会比较麻烦
@Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {

        httpSecurity

                // 禁用 CSRF
                .csrf().disable()
                // 不创建会话
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 过滤请求
                .authorizeRequests()
                .antMatchers(
                        HttpMethod.GET,
                        "/*.html",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js"
                ).anonymous()
                .antMatchers( HttpMethod.POST,"/auth/login).permitAll()
                .antMatchers("/websocket/**").anonymous()
                // 所有请求都需要认证
              .anyRequest().authenticated()
                // 防止iframe 造成跨域
                .and().headers().frameOptions().disable();
          // 添加自定义拦截器
        httpSecurity
                .addFilterBefore(tokenFilter,UsernamePasswordAuthenticationFilter.class);
}}

 

 步骤4:自定义拦截器,通过此拦截器, 前端访问时候头部要带"Authorization",通过token获取用户信息

@Component
public class JwtTokenFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String requestHeader = request.getHeader("Authorization");
String authToken=null;
if (requestHeader != null && requestHeader.startsWith("Bearer ")) {
    authToken = requestHeader.substring(7);
    String userName =Jwts.parser().setSigningKey(secret).parseClaimsJws(authToken ).getBody().getSubject();

}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
SystemUser userDetails = (SystemUser ) this.userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
}
}
}

 

步骤五:登录返回toekn 给前端

@Getter
@AllArgsConstructor
public class AuthenticationInfo implements Serializable {

    private final String token;

    private final JwtUser user;
}
// 登录构造器

@RequestMapping("auth")
public class SecurityController{
@Autowired
@Qualifier("SystemUserDetailsService")
private UserDetailsService userDetailsService;
@PostMapping(value = "${jwt.auth.path}")
public AuthorizationUser login(@RequestParam("userName")String userName,@RequestParam("password")String password)) {
final SystemUser jwtUser = (SystemUser ) userDetailsService.loadUserByUsername(userName);
//获取用户的token,是否存在
Date expirationDate = new Date(createdDate.getTime() +864000);
String token =Jwts.builder()
        .setClaims(claims)
        .setSubject()
        .setIssuedAt(new Date)
        .setExpiration(expirationDate)
        .signWith(SignatureAlgorithm.HS512, secret)
        .compact();
return new AuthenticationInfo(token, jwtUser));
}

}

 

步骤6 定义具有某个菜单的构造器,前端通过定义菜单标识跟后台@PreAuthorize 对应的权限进行关联起来,这样就可以形成对应的权限

@RequestMapping("/admin")
public class Demo {

@RequestMapping("/pageList")
@PreAuthorize("hasAnyRole(‘menu_mark’)")
public List<String> pageList(){
return new ArrayList();
}
}