SpringBoot+SpringSecurity+SpringSession实现一个前后端分离的权限管理系统

版权声明:本文为博主原创文章,未经博主允许不得转载 https://blog.csdn.net/qq_27948811/article/details/89840329

这里SpringBoot用2.0.5版本

一、准备工作

1、主要依赖如下:


dependencies {    
    compile('com.alibaba:druid:1.1.5')
    compile('com.baomidou:mybatis-plus-boot-starter:2.2.0')
    compile('org.springframework.boot:spring-boot-starter-security')

    compile('org.springframework.boot:spring-boot-starter-data-redis')
// 如果通过redis来存储session就需要下面这个依赖
//    compile ('org.springframework.session:spring-session-data-redis')
// 用jdbc来存储session
    compile('org.springframework.session:spring-session-jdbc')
    compile('org.springframework.boot:spring-boot-starter-data-rest')

    compile('org.springframework.boot:spring-boot-starter-web')
    compile('org.springframework.boot:spring-boot-starter-undertow')
    compile('org.springframework.boot:spring-boot-starter-log4j2')
    compile('org.springframework.boot:spring-boot-configuration-processor')

    compile('org.springframework.boot:spring-boot-starter-test')
    compile('com.alibaba:fastjson:1.2.40')
    runtime('com.microsoft.sqlserver:mssql-jdbc')
    compile('org.apache.commons:commons-lang3:3.5')
    compile("io.springfox:springfox-swagger-ui:2.6.0")
    compile("io.springfox:springfox-swagger2:2.6.0")
    compile('org.projectlombok:lombok:1.16.16')
}
configurations {
    all*.exclude module: 'spring-boot-starter-logging'
    all*.exclude module: 'logback-classic'
    all*.exclude module: 'log4j-over-slf4j'
    all*.exclude module: 'spring-boot-starter-tomcat'
}

2、application.yml:

cors:
  allowedOrigins: "*"
  allowedMethods: GET,POST,OPTIONS
  allowCredentials: true
  allowedHeaders: '*'

spring:
  datasource:
    url: jdbc:sqlserver://localhost:1433;DatabaseName=test
    username: admin
    password: 123456
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
    druid:
      filters: stat,wall        # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
      driver-class-name: com.mysql.jdbc.Driver
      initial-size: 5
      min-idle: 1
      max-active: 30
      max-wait: 60000
      time-between-eviction-runs-millis: 60000
      validation-query: SELECT 1
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      filter:
        stat:
          log-slow-sql: true
          slow-sql-millis: 5000
          merge-sql: true
      max-pool-prepared-statement-per-connection-size: 500
      connect-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=50000
      min-evictable-idle-time-millis: 600000   # 配置一个连接在池中生存的时间,单位是毫秒
      max-evictable-idle-time-millis: 900000
  session:
    store-type: jdbc
    timeout: 3600s
    jdbc:
      cleanup-cron: 0 */30 * * * ?  #半小时清理session,cron表达式可以自行百度
      table-name: SPRING_SESSION
      initialize-schema: embedded
      schema: classpath:schema-sqlserver.sql
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8

  servlet:
    multipart:
      max-file-size: 10MB
      enabled: true
      max-request-size: 10MB
mybatis-plus:
  type-aliases-package: com.test.domain.po
  mapper-locations: classpath:mapper/*.xml
  global-config:
    id-type: 0
    refresh-mapper: true
    db-column-underline: true
    logic-delete-value: 1
    logic-not-delete-value: 0
  configuration:
    cache-enabled: false
    map-underscore-to-camel-case: true
server:
  port: 9000
  compression:
    enabled: true
    mime-types: application/json,application/mapper,text/html,text/mapper,text/plain

注意:schema-sqlserver.sql这个sql文件来自spring-session-jdbc-2.0.6.RELEASE.jar包

因为里面的PRINCIPAL_NAME字段长度是根据登陆接口返回的对象来的;如果你的登陆报错数据库字段超长(sql server是报错字符被截断),则检查一下这个SPRING_SESSION表的某些字段是不是太短了;有些登陆接口可能返回了很多信息,spring自带的这个sql的PRINCIPAL_NAME字段长度不够,我这里修改了,放到了resource目录;

 建表语句如下:

CREATE TABLE SPRING_SESSION (
	PRIMARY_ID CHAR(36) NOT NULL,
	SESSION_ID CHAR(36) NOT NULL,
	CREATION_TIME BIGINT NOT NULL,
	LAST_ACCESS_TIME BIGINT NOT NULL,
	MAX_INACTIVE_INTERVAL INT NOT NULL,
	EXPIRY_TIME BIGINT NOT NULL,
	PRINCIPAL_NAME VARCHAR(2000),
	CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (PRIMARY_ID)
);

CREATE UNIQUE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (SESSION_ID);
CREATE INDEX SPRING_SESSION_IX2 ON SPRING_SESSION (EXPIRY_TIME);
CREATE INDEX SPRING_SESSION_IX3 ON SPRING_SESSION (PRINCIPAL_NAME);

CREATE TABLE SPRING_SESSION_ATTRIBUTES (
	SESSION_PRIMARY_ID CHAR(36) NOT NULL,
	ATTRIBUTE_NAME VARCHAR(200) NOT NULL,
	ATTRIBUTE_BYTES IMAGE NOT NULL,
	CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_PRIMARY_ID, ATTRIBUTE_NAME),
	CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_PRIMARY_ID) REFERENCES SPRING_SESSION(PRIMARY_ID) ON DELETE CASCADE
);

 如果session存储是用redis则application.yml中的session配置则改为:

并且需要在启动类上加上注解:@EnableRedisHttpSession

3、上面都是准备工作,下面直接上代码

跨域配置:

@Data
@Component
@ConfigurationProperties(prefix = "cors")
public class CorsProperties {
    /**
     * 允许请求的域名
     */
    private List<String> allowedOrigins = new ArrayList<>();

    /**
     * 允许请求http的方法
     */
    private List<String> allowedMethods = new ArrayList<String>();

    /**
     * 允许请求http头信息
     */
    private List<String> allowedHeaders = new ArrayList<String>();

    /**
     * 排除请求http头信息
     */
    private List<String> exposedHeaders = new ArrayList<String>();

    /**
     * Options请求的最大缓存时间
     */
    private long maxAge = 60;

    /**
     * 是否允许请求时携带Cookie
     */
    private Boolean allowCredentials = false;
}
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebAppConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //注册自定义拦截器,添加拦截路径和排除拦截路径
        registry.addInterceptor(new InterceptorConfig()).addPathPatterns("/v1/**/**");
    }

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                // 放行哪些原始域
                .allowedOrigins("*")
                // 是否发送cookie
                .allowCredentials(true)
                // 放行哪些请求
                .allowedMethods("GET", "POST", "OPTIONS", "DELETE", "PUT")
                // 放行哪些header
                .allowedHeaders("*")
                // 暴露哪些头部信息(因为跨域访问默认不能获取全部header
                .exposedHeaders("Header1", "Header2");
    }
}

拦截器:

@Slf4j
public class InterceptorConfig implements HandlerInterceptor {

    /**
     * 进入controller层之前拦截请求
     *
     * @param request
     * @param response
     * @param o
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
        log.info("---------------------开始进入请求地址拦截----------------------------");
        // 从security的context中获取登陆用户信息
        // UserVO vo = (UserVO) SecurityContextHolder
        //         .getContext()
        //         .getAuthentication()
        //         .getPrincipal();
		// 这里可以加自己的逻辑
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

swagger配置:

@Configuration
@EnableSwagger2
public class SwaggerConfig{

    @Bean
    public Docket createDocket() {
        Predicate<String> path = or(
                ant("/v1/**"));
        return new Docket(DocumentationType.SWAGGER_2)
                .useDefaultResponseMessages(false)
                .forCodeGeneration(false)
                .apiInfo(apiInfo())
                .select()
                .paths(path)
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("权限管理系统")
                .description("权限管理系统")
                .build();
    }
}

 SpringSecurity配置:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.vote.UnanimousBased;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.access.expression.WebExpressionVoter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Arrays;
import java.util.List;

import static java.util.stream.Collectors.toList;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private MenusService menuService;

    /**
     * 设置验证信息
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        super.configure(auth);
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
		// 下面这行是放行所有请求,不校验权限
        // web.ignoring().antMatchers("/v1/**"); 
        web.debug(true);
        super.configure(web);
    }

    /**
     * 设置权限信息
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //主要是看UrlMatchVoter,所有的权限检查都在UrlMatchVoter
        http.cors()
                .and()
                .csrf() 
                .disable()
                .authorizeRequests()
                .antMatchers("/v1/**").permitAll()
                .accessDecisionManager(accessDecisionManager())
        ;
    }

    @Bean
    public AccessDecisionManager accessDecisionManager() {
		// menuService查询所有的菜单url
        List<UrlGrantedAuthority> collect = menuService
                .allMenu()
                .stream()
                .map(m -> new UrlGrantedAuthority(m.getUrl()))
                .collect(toList());

        List<AccessDecisionVoter<? extends Object>> decisionVoters
                = Arrays.asList(
                new WebExpressionVoter(),
                new UrlMatchVoter(collect));
        return new UnanimousBased(decisionVoters);
    }

    /**
     * 跨域请求配置
     *
     * @param properties 配置属性文件名
     * @return
     */
    @Bean
    public CorsConfigurationSource corsConfigurationSource(CorsProperties properties) {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(properties.getAllowedOrigins());
        configuration.setAllowedMethods(properties.getAllowedMethods());
        configuration.setAllowCredentials(properties.getAllowCredentials());
        configuration.setAllowedHeaders(properties.getAllowedHeaders());
        configuration.setExposedHeaders(properties.getExposedHeaders());
        configuration.setMaxAge(properties.getMaxAge());
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}
import lombok.EqualsAndHashCode;
import org.springframework.security.core.GrantedAuthority;

@EqualsAndHashCode
public class UrlGrantedAuthority implements GrantedAuthority {
    private final String url;

    public UrlGrantedAuthority(String url) {
        assert url != null;
        this.url = url.trim();
    }

    /**
     * 返回需要认证的URL地址
     *
     * @return
     */
    @Override
    public String getAuthority() {
        return url;
    }
}
import com.google.common.collect.ImmutableList;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.util.Collection;
import java.util.List;
/**
 * ULR投票器
 * @author 
 */
public class UrlMatchVoter implements AccessDecisionVoter<FilterInvocation> {
    private final ImmutableList<UrlGrantedAuthority> defaultAuthorities;

    public UrlMatchVoter(List<UrlGrantedAuthority> defaultAuthorities) {
        this.defaultAuthorities = ImmutableList.copyOf(defaultAuthorities);
    }

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

    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }

    @Override
    public int vote(Authentication authentication, FilterInvocation fi, Collection<ConfigAttribute> attributes) {
        assert authentication != null;

        //默认不进行权限检查的URL
        boolean checkResult = check(fi.getRequest(), defaultAuthorities);
        if (checkResult) {
            return ACCESS_GRANTED;
        }

        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        checkResult = check(fi.getRequest(), authorities);
        return checkResult ? ACCESS_GRANTED : ACCESS_DENIED;
    }

    private boolean check(HttpServletRequest request, Collection<? extends GrantedAuthority> authorities) {
        for (GrantedAuthority authority : authorities) {
            if (!(authority instanceof UrlGrantedAuthority)) {
                continue;
            }

            UrlGrantedAuthority urlGrantedAuthority = (UrlGrantedAuthority) authority;
            if (StringUtils.isEmpty(urlGrantedAuthority.getAuthority())) {
                continue;
            }

            AntPathRequestMatcher antPathRequestMatcher =
                    new AntPathRequestMatcher(urlGrantedAuthority.getAuthority());
            if (antPathRequestMatcher.matches(request)) {
                return true;
            }
        }

        return false;
    }
}

4、菜单表可以根据自己需求设计: 

5、controller:


import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.validation.Errors;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

@Api(description = "用户管理")
@RestController
@RequestMapping("/v1/user")
@Slf4j
public class UserController extends BaseController {
    @Autowired
    private UserService service;
     @Autowired
    private MenusService menusService;
   

    @ApiOperation("登录")
    @PostMapping("/login")
    public Result<UserVO> login(@Validated @RequestBody LoginForm form, HttpServletRequest request) {
        UserEntity po = service.login(form.getUserName(), form.getPassword());
        if (po == null) {
            return Result.fail("登录失败");
        }

        UserVO vo = new UserVO();
        BeanUtils.copyProperties(po, vo);

        //查询菜单权限
        List<MenusEntity> menus = menusService.selectMenusByRoleCode(vo.getRoleCode());
        if (CollectionUtils.isEmpty(menus)) {
            throw new RuntimeException("权限不足");
        }

        // 将该用户的菜单权限放入security上下文中
        Collection<GrantedAuthority> autos = new ArrayList<>();
        menus.forEach(m -> autos.add(new UrlGrantedAuthority(m.getUrl())));
        SecurityContextHolder.getContext()
                .setAuthentication(
                        new UsernamePasswordAuthenticationToken(vo,
                                form.getPassword(), autos));
        return Result.success(vo);
    }

    @ApiOperation("注销")
    @PostMapping("/logout")
    public Result logout() {
        SecurityContextHolder.clearContext();
        return Result.success();
    }

}

就这样就大功告成了,下面我们访问swagger测试一下http://localhost:9000/swagger-ui.html

先不登陆,访问某个接口试试:

这里报错,Access Denied,如果登陆了但是没有这个接口的权限也会报这个错;

然后登陆一下:

然后访问刚刚的接口:

访问成功了,然后看看数据库已经插入了一条session的记录:

 如果是使用redis存储session,redis对应也会有相应的记录,这里就不截图了;

如果有问题,可以留言交流

猜你喜欢

转载自blog.csdn.net/qq_27948811/article/details/89840329