- 1. Spring Secutity相关概念介绍
- 1.1 相关术语基本概念
- 1.2 RBAC授权控制
- 1.3 Spring Secutity介绍
- 2. 基于SpringBoot+mybatis-plus+Spring Security认证授权项目的快速搭建(非分布式)
- 2.1 基于SpringBoot+Spring Security认证授权项目的快速搭建
- 2.1.1 创建一maven工程
- 2.1.2 在pom文件中导入依赖
- 2.1.3 在resources包下创建application.yml的配置文件
- 2.1.4 在Java包下面创建Servlet Context配置—WebConfig类
- 2.1.5 在配置文件包中配置springsecurity配置
- 2.1.6 创建用户登录的controller类
- 2.1.7 在配置类的上层包中创建启动类
- 2.1.8 启动项目
- 2.2 基于SpringBoot+mybatis-plus+Spring Security认证授权项目的搭建
- 2.2.1 在1.1的基础之上整合mybatis-plus,首先在pom文件中添加相关依赖。
- 2.2.2 在application.xml配置文件中添加数据源和mybatis-plus配置。
- 2.2.3 创建一个名为StartGenerator的代码生成器类。
- 2.2.4 在resource文件夹中的templates文件夹中创建实体类和mapper.xml的文件模板entity.java.ftl、mapper.xml.ftl。
- 2.2.5 用代码生成器生成下面几张表的实体类以及mapper文件等。
- 2.2.6 修改springsecurity配置文件,如下:
- 2.2.7 测试.
- 认证: 用户认证就是判断一个用户的身份是否合法的过程,用户去访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问,不合法则拒绝访问。常见的用户身份认证方式有:用户名密码登录,二维码登录,手机短信登录,指纹认证等方式。
- 授权: 根据用户的权限来控制用户使用资源的过程。
- 会话: 系统为了保持当前用户的登录状态所提供的机制,常见的有基于session方式、基于token方式等,用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保证在会话中。
- 基于session的认证方式: 它的交互流程是,用户认证成功后,在服务端生成用户相关的数据保存在session(当前会话)中,发给客户端的sesssion_id 存放到cookie 中,这样用户客户端请求时带上session_id 就可以验证服务器端是否存在 session 数据,以此完成用户的合法校验,当用户退出系统或session过期销毁时,客户端的session_id也就无效了。
- 基于token方式: 它的交互流程是,用户认证成功后,服务端生成一个token发给客户端,客户端可以放到 cookie 或 localStorage等存储中,每次请求时带上 token,服务端收到token通过验证后即可确认用户身份。
基于session的认证方式由Servlet规范定制,服务端要存储session信息需要占用内存资源,客户端需要支持cookie;基于token的方式则一般不需要服务端存储token,并且不限制客户端的存储方式。如今移动互联网时代更多类型的客户端需要接入系统,系统多是采用前后端分离的架构进行实现,所以基于token的方式更适合。
- RBAC基于角色的访问控制(Role-based Access Control): 是按角色进行授权,比如:主体的角色为总经理可以查询企业运营报表,查询员工工资信息等。
- RBAC基于资源的访问控制(Resource-based Access Control): 按资源(或权限)进行授权,比如:用户必须具有查询工资权限才可以查询员工工资信息等。
优点:系统设计时定义好查询工资的权限标识,即使查询工资所需要的角色变化为总经理和部门经理也不需要修改授权代码,系统可扩展性强。
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。由于它是Spring生态系统中的一员,因此它伴随着整个Spring生态系统不断修正、升级,在spring boot项目中加入springsecurity更是十分简单,使用Spring Security 减少了为企业系统安全控制编写大量重复代码的工作。
2. 基于SpringBoot+mybatis-plus+Spring Security认证授权项目的快速搭建(非分布式) 2.1 基于SpringBoot+Spring Security认证授权项目的快速搭建 2.1.1 创建一maven工程 2.1.2 在pom文件中导入依赖2.1.3 在resources包下创建application.yml的配置文件org.springframework.boot spring-boot-starter-securityorg.springframework.boot spring-boot-starter-webjavax.servlet javax.servlet-api4.0.1 provided javax.servlet jstlorg.projectlombok lomboktrue org.springframework.boot spring-boot-starter-testtest org.springframework.security spring-security-testtest org.springframework.boot spring-boot-maven-pluginorg.projectlombok lombok
server:
port: 8080
servlet:
context-path: /security-springboot
spring:
application:
name: security-springboot
mvc:
view:
prefix: /WEB-INF/views
suffix: .jsp
2.1.4 在Java包下面创建Servlet Context配置—WebConfig类
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("redirect:/login");
}
}
2.1.5 在配置文件包中配置springsecurity配置
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//配置用户信息服务
@Bean
@Override
public UserDetailsService userDetailsService(){
//实际开发中该处读取数据库中的用户数据 此处为构造的测试数据
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
return manager;
}
@Bean
public PasswordEncoder passwordEncoder(){
//采用不加密的形式比较密码
return NoOpPasswordEncoder.getInstance();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/r/r1").hasAuthority("p1") //web路径权限添加
.antMatchers("/r/r2").hasAuthority("p2")
.antMatchers("/r
@GetMapping(value = "/r/r1")
public String r1(){
return " 访问资源1";
}
@GetMapping(value = "/r/r2")
public String r2(){
return " 访问资源2";
}
}
2.1.7 在配置类的上层包中创建启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringsecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SpringsecurityApplication.class, args);
}
}
2.1.8 启动项目
登录认证
2.2.2 在application.xml配置文件中添加数据源和mybatis-plus配置。1.8 4.0.1 2.3.28 1.2.47 3.2.0 3.2.0 1.7 4.1.6 2.9.2 1.5.21 2.9.2 org.springframework.boot spring-boot-starter-securityorg.springframework.boot spring-boot-starter-webjavax.servlet javax.servlet-api${servlet.version} provided javax.servlet jstlorg.projectlombok lomboktrue org.springframework.boot spring-boot-starter-testtest org.springframework.security spring-security-testtest mysql mysql-connector-javaruntime com.baomidou mybatis-plus-boot-starter${mybatis-plus.version} com.baomidou mybatis-plus-generator${mybatis-plus-generator.version} org.freemarker freemarker${freemarker.version} com.alibaba fastjson${fastjson.version} org.apache.velocity velocity${velocity.version} com.github.pagehelper pagehelper${pagehelper.version} io.springfox springfox-swagger2${springfox-swagger2.version} io.swagger swagger-annotationsio.swagger swagger-annotations${swagger-annotations.version} io.springfox springfox-swagger-ui${springfox-swagger-ui.version}
server:
port: 8281
servlet:
context-path: /security-springboot
spring:
application:
name: security-springboot
#视图解析
mvc:
view:
prefix: /WEB-INF/views
suffix: .jsp
#数据源配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
url: jdbc:mysql://127.0.0.1:3306/test?serverTimezone=GMT%2B8&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false
initialSize: 2
minIdle: 2
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECt 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
#mybatis-plus相关配置
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
auto-mapping-behavior: full
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath*:mapper*Mapper.xml
# 逻辑删除配置
global-config:
db-config:
logic-delete-value: 1
logic-not-delete-value: 0
2.2.3 创建一个名为StartGenerator的代码生成器类。
package generator;
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import org.apache.commons.lang.StringUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
public class StartGenerator {
public static String url = "jdbc:mysql://127.0.0.1:3306/test?serverTimezone=GMT%2B8&characterEncoding=utf8&zeroDateTimeBehavior" +
"=convertToNull";
public static String userName = "root";
public static String userPwd = "123456";
public static void main(String[] args) {
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
// 文件输出路径
gc.setOutputDir(projectPath + "/src/main/java");
// 作者
gc.setAuthor("immortal");
gc.setOpen(false);
gc.setSwagger2(true);
gc.setbaseResultMap(true);
gc.setbaseColumnList(true);
// 不覆盖已有,则为false
gc.setFileOverride(false);
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl(StartGenerator.url);
dsc.setDriverName("com.mysql.jdbc.Driver");
dsc.setUsername(StartGenerator.userName);
dsc.setPassword(StartGenerator.userPwd);
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
pc.setModuleName("xmlMapper");
pc.setParent("com.immortal");
mpg.setPackageInfo(pc);
// 自定义配置
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
// to do nothing
}
};
List focList = new ArrayList<>();
focList.add(new FileOutConfig("/templates/mapper.xml.ftl") {
@Override
public String outputFile(TableInfo tableInfo) {
// 自定义输入文件路径和名称
return projectPath + "/src/main/java/com/immortal/springsecurity/mapper/userMapper" + pc.getModuleName() + "/" + tableInfo.getEntityName() +
"Mapper" + StringPool.DOT_XML;
}
});
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);
mpg.setTemplate(new TemplateConfig().setXml(null));
// 策略配置
StrategyConfig strategy = new StrategyConfig();
// 表名生成策略
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
strategy.setSuperEntityClass("");
strategy.setEntityLombokModel(true);
strategy.setRestControllerStyle(true);
strategy.setSuperControllerClass(null);
// 多个表名传数组
strategy.setInclude(tableArray());
strategy.setSuperEntityColumns("id");
strategy.setControllerMappingHyphenStyle(true);
strategy.setTablePrefix(pc.getModuleName() + "_");
strategy.setVersionFieldName("version");
strategy.setLogicDeleteFieldName("deleted");
strategy.setEntityColumnConstant(true);
mpg.setStrategy(strategy);
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
mpg.execute();
}
public static String scanner(String tip) {
Scanner scanner = new Scanner(System.in);
StringBuilder help = new StringBuilder();
help.append("请输入" + tip + ":");
System.out.println(help.toString());
if (scanner.hasNext()) {
String ipt = scanner.next();
if (StringUtils.isNotEmpty(ipt)) {
return ipt;
}
}
throw new MybatisPlusException("请输入正确的" + tip + "!");
}
public static String[] tableArray() {
try {
List tableList = new ArrayList<>(16);
// 多表生成
tableList.add("t_user");
tableList.add("t_role");
tableList.add("t_permission");
return tableList.toArray(new String[tableList.size()]);
} catch (Exception e) {
throw new MybatisPlusException("请输入正确的表名!");
}
}
}
2.2.4 在resource文件夹中的templates文件夹中创建实体类和mapper.xml的文件模板entity.java.ftl、mapper.xml.ftl。
entity.java.ftl文件:
package ${package.Entity};
<#list table.importPackages as pkg>
import ${pkg};
#list>
<#if swagger2>
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
#if>
<#if entityLombokModel>
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
#if>
import java.util.Date;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
<#if entityLombokModel>
@Data
<#if superEntityClass??>
@EqualsAndHashCode(callSuper = true)
<#else>
@EqualsAndHashCode(callSuper = false)
#if>
@Accessors(chain = true)
#if>
<#if table.convert>
@TableName("${table.name}")
#if>
<#if swagger2>
@ApiModel(value="${entity}对象", description="${table.comment!}")
#if>
<#if superEntityClass??>
public class ${entity} extends ${superEntityClass}<#if activeRecord><${entity}>#if> {
<#elseif activeRecord>
public class ${entity} extends Model<${entity}> {
<#else>
public class ${entity} implements Serializable {
#if>
private static final long serialVersionUID = 1L;
<#-- ---------- BEGIN 字段循环遍历 ---------->
<#list table.fields as field>
<#if field.keyFlag>
<#assign keyPropertyName="${field.propertyName}"/>
#if>
<#if field.comment!?length gt 0>
<#if swagger2>
@ApiModelProperty(value = "${field.comment}")
<#else>
#if>
#if>
<#if field.keyFlag>
@TableId(value="${field.name}", type = IdType.INPUT)
<#-- 普通字段 -->
<#elseif field.fill??>
<#-- ----- 存在字段填充设置 ----->
<#if field.convert>
@TableField(value = "${field.name}", fill = FieldFill.${field.fill})
<#else>
@TableField(fill = FieldFill.${field.fill})
#if>
<#elseif field.convert>
@TableField("${field.name}")
#if>
<#-- 乐观锁注解 -->
<#if (versionFieldName!"") == field.name>
@Version
#if>
<#-- 逻辑删除注解 -->
<#if (logicDeleteFieldName!"") == field.name>
@TableLogic
#if>
private ${(field.propertyType == 'LocalDateTime')?string('Date',field.propertyType)} ${field.propertyName};
#list>
<#------------ END 字段循环遍历 ---------->
<#if !entityLombokModel>
<#list table.fields as field>
<#if field.propertyType == "boolean">
<#assign getprefix="is"/>
<#else>
<#assign getprefix="get"/>
#if>
public ${field.propertyType} ${getprefix}${field.capitalName}() {
return ${field.propertyName};
}
<#if entityBuilderModel>
public ${entity} set${field.capitalName}(${field.propertyType} ${field.propertyName}) {
<#else>
public void set${field.capitalName}(${field.propertyType} ${field.propertyName}) {
#if>
this.${field.propertyName} = ${field.propertyName};
<#if entityBuilderModel>
return this;
#if>
}
#list>
#if>
<#if entityColumnConstant>
<#list table.fields as field>
public static final String ${field.name?upper_case} = "${field.name}";
#list>
#if>
<#if activeRecord>
@Override
protected Serializable pkVal() {
<#if keyPropertyName??>
return this.${keyPropertyName};
<#else>
return null;
#if>
}
#if>
<#if !entityLombokModel>
@Override
public String toString() {
return "${entity}{" +
<#list table.fields as field>
<#if field_index==0>
"${field.propertyName}=" + ${field.propertyName} +
<#else>
", ${field.propertyName}=" + ${field.propertyName} +
#if>
#list>
"}";
}
#if>
}
mapper.xml.ftl文件:
2.2.5 用代码生成器生成下面几张表的实体类以及mapper文件等。<#if enableCache> #if> <#if baseResultMap> <#list table.fields as field> <#if field.keyFlag><#--生成主键排在第一位--> #if> <#if baseColumnList>#if> #list> <#list table.commonFields as field><#--生成公共字段 --> #list> <#list table.fields as field> <#if !field.keyFlag><#--生成普通字段 --> #if> #list> <#list table.commonFields as field> ${field.name}, #list> ${table.fieldNames} #if>
用户表
用户-角色表
角色权限表
角色表
角色-权限表
表结构描述:张三是角色1,角色1拥有p1和p3权限,能访问r/r1和/r/r3路径;李四是角色2,角色2拥有p2权限,能访问/r/r2路径。
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.immortal.springsecurity.entity.TPermission;
import com.immortal.springsecurity.entity.TUser;
import com.immortal.springsecurity.mapper.userMapper.UserMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements UserDetailsService {
@Resource
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper userQueryWrapper = new QueryWrapper<>();
userQueryWrapper.eq(TUser.USERNAME,username);
TUser user = userMapper.selectOne(userQueryWrapper);
List authorityByUserId = userMapper.getAuthorityByUserId(user.getId());
List authoritys = new ArrayList<>();
authorityByUserId.iterator().forEachRemaining(c -> authoritys.add(c.getCode()));
String[] auths = new String[authoritys.size()];
authoritys.toArray(auths);
if(user != null){
UserDetails userDetails =
org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.authorities(auths)
.build();
return userDetails;
}
return null;
}
@Bean
public PasswordEncoder passwordEncoder(){
//采用不加密的形式比较密码
return NoOpPasswordEncoder.getInstance();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/r/r1").hasAuthority("p1") //web路径权限添加
.antMatchers("/r/r2").hasAuthority("p2")
.antMatchers("/r/r3").hasAuthority("p3")
.antMatchers("/r/**").authenticated()
.anyRequest().permitAll()
.and()
.formLogin().successForwardUrl("/loginsuccess"); //允许表单登录 登录成功后调转到该路径
}
}
2.2.7 测试.
张三登录成功并能访问/r/r1和/r/r3路径;李四登录成功只能访问/r/r2;为了方便测试,此处密码的比对还是采用不加密方式。



