这里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 allowedOrigins = new ArrayList<>();
/**
* 允许请求http的方法
*/
private List allowedMethods = new ArrayList();
/**
* 允许请求http头信息
*/
private List allowedHeaders = new ArrayList();
/**
* 排除请求http头信息
*/
private List exposedHeaders = new ArrayList();
/**
* 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 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 collect = menuService
.allMenu()
.stream()
.map(m -> new UrlGrantedAuthority(m.getUrl()))
.collect(toList());
List> 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 {
private final ImmutableList defaultAuthorities;
public UrlMatchVoter(List 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 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 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 menus = menusService.selectMenusByRoleCode(vo.getRoleCode());
if (CollectionUtils.isEmpty(menus)) {
throw new RuntimeException("权限不足");
}
// 将该用户的菜单权限放入security上下文中
Collection 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对应也会有相应的记录,这里就不截图了;
如果有问题,可以留言交流