记录一下近期学习 SpringSecurity 第一阶段的笔记和心得,并与之前的学习框架进行整合,有不妥之处,忘大佬们指出!
目录
2.3、利用Add Framework Support添加SpringMVC
2.2节,MyBatis、MySql 和 Druid连接池等依赖已经添加
3.4、利用 MyBatis Generator 自动生成接口文件和 xml 文件
4.4、自定义拦截器 MyFilterSecurityInterceptor
4.5、自定义资源获取类 MyFilterInvocationSecurityMetadataSource
4.6、自定义决策器 MyAccessDecisionManager
1、数据库构建
构建sql语句及其对应说明
##用户表
CREATE TABLE `user` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
);
##角色表
CREATE TABLE `role` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
);
##角色用户关联表
CREATE TABLE `user_role` (
`user_id` bigint(11) NOT NULL,
`role_id` bigint(11) NOT NULL
);
##权限表
CREATE TABLE `permission` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`url` varchar(255) NOT NULL,
`name` varchar(255) NOT NULL,
`description` varchar(255) NULL
PRIMARY KEY (`id`)
);
##角色权限关联表
CREATE TABLE `role_permission` (
`role_id` bigint(11) NOT NULL,
`permission_id` bigint(11) NOT NULL
);
##构建两个用户,password与username相同,md5加密
INSERT INTO user (id, username, password) VALUES (1,'user','ee11cbb19052e40b07aac0ca060c23ee');
INSERT INTO user (id, username , password) VALUES (2,'admin','21232f297a57a5a743894a0e4a801fc3');
##构建两个角色
INSERT INTO role (id, name) VALUES (1,'USER');
INSERT INTO role (id, name) VALUES (2,'ADMIN');
##构建权限
INSERT INTO permission (id, url, name, pid) VALUES (1,'/user/common','common',0);
INSERT INTO permission (id, url, name, pid) VALUES (2,'/user/admin','admin',0);
##用户角色关系表
INSERT INTO user_role (user_id, role_id) VALUES (1, 1);
INSERT INTO user_role (user_id, role_id) VALUES (2, 1);
INSERT INTO user_role (user_id, role_id) VALUES (2, 2);
##构建角色权限关系表
INSERT INTO role_permission (role_id, permission_id) VALUES (1, 1);
INSERT INTO role_permission (role_id, permission_id) VALUES (2, 1);
INSERT INTO role_permission (role_id, permission_id) VALUES (2, 2);
2、通过注解形式,搭建SpringMVC框架
2.1、通过Maven创建一个project
2.2、在pom文件中添加依赖(包括本文章所有阶段的依赖)
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<!--**********JunitTest**********-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<!--**********SpringMVC**********-->
<!-- https://mvnrepository.com/artifact/org.springframework/spring-web -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.2.0.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.0.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.0.RELEASE</version>
</dependency>
<!--**********jstl**********-->
<!-- https://mvnrepository.com/artifact/javax.servlet.jsp.jstl/jstl -->
<dependency>
<groupId>javax.servlet.jsp.jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/javax.servlet.jsp.jstl/jstl-api -->
<dependency>
<groupId>javax.servlet.jsp.jstl</groupId>
<artifactId>jstl-api</artifactId>
<version>1.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.glassfish.web/jstl-impl -->
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>jstl-impl</artifactId>
<version>1.2</version>
</dependency>
<!--**********日志工具**********-->
<!-- https://mvnrepository.com/artifact/commons-logging/commons-logging -->
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.12.1</version>
</dependency>
<!--**********MySQL驱动类**********-->
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.18</version>
</dependency>
<!--**********Alibaba数据库连接池类**********-->
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.21</version>
</dependency>
<!--**********lombok**********-->
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
<scope>provided</scope>
</dependency>
<!--**********mybatis**********-->
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mybatis.generator/mybatis-generator-core -->
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.4.0</version>
</dependency>
<!--**********servlet工具类**********-->
<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
2.3、利用Add Framework Support添加SpringMVC
2.4、创建JavaConfig注解配置类
WebAppInitializer(后期添加 SpringSecurity 框架时,会对其进行修改)
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[]{RootConfig.class};
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[]{WebConfig.class};
}
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
}
WebConfig
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
@Configuration
@EnableWebMvc
@ComponentScan("com.zjst.tc.controller")
public class WebConfig implements WebMvcConfigurer {
/**
* 视图解析器
* @return
*/
@Bean
public ViewResolver viewResolver(){
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/views/");
viewResolver.setSuffix(".jsp");
viewResolver.setExposeContextBeansAsAttributes(true);
return viewResolver;
}
/**
* 配置静态资源处理
* @param configurer
*/
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();//将静态资源的请求转发到servlet容器中默认的servlet上
}
}
RootConfig(暂时为空,后续会添加 druid 和 mybatis 配置)
@Configuration
@ComponentScan(basePackages = {"com.zjst.tc"},
excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Controller.class, EnableWebMvc.class})},
includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Service.class, Repository.class, Component.class})}
)
public class RootConfig implements EmbeddedValueResolverAware {
}
2.5、添加测试 controller 和 JSP
SuccessController
@Controller
public class SuccessController {
@RequestMapping(value="/success",method = RequestMethod.GET)
public String success(){
return "success";
}
}
index.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html>
<head>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<a href="/success">test</a>
</body>
</html>
success.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>this is success page</h1>
</body>
</html>
2.6、此时可以将项目添加至 tomcat 中进行测试
index 页面成功跳转 success 页面即为成功,当然也可以引入 MockMVC 进行测试
3、整合 MyBatis 和 Druid
3.1、在 pom 文件中添加依赖
2.2节,MyBatis、MySql 和 Druid连接池等依赖已经添加
3.2 添加数据库 properties 配置文件
各配置项的意义可以参考:https://www.cnblogs.com/wuyun-blog/p/5679073.html
dbConfig.properties
type=com.alibaba.druid.pool.DruidDataSource
user=root
password=1230
jdbcUrl=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF8&serverTimezone=UTC&useSSL=false
driverClass=com.mysql.cj.jdbc.Driver
##初始连接数量
initialSize=5
##最小空闲数量
minIdle=5
##最大连接数量
maxActive=20
##最大等待毫秒数
maxWait=60000
timeBetweenEvictionRunsMillis=60000
minEvictableIdleTimeMillis=300000
##用来检测连接是否有效的sql,要求是一个查询语句
validationQuery=SELECT 1 FROM DUAL
##申请连接的时候检测,如果空闲时间大于#timeBetweenEvictionRunsMillis,#执行validationQuery检测连接是否有效。
testWhileIdle=true
##申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能
testOnBorrow=false
##归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能
testOnReturn=false
poolPreparedStatements=true
maxPoolPreparedStatementPerConnectionSize=20
filters=stat
connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
3.3、利用 JavaConfig 配置 MyBatis
在2.4节的RootConfig中添加,实现了 EmbeddedValueResolverAware 接口,用于将 properties 中的配置项引入,也可使用 @Value("...")注解将配置项引入
import com.alibaba.druid.pool.DruidDataSource;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.context.annotation.*;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.util.StringValueResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@Configuration
@MapperScan("com.zjst.tc.repository.mappers")//用于扫描mybatis的mapper接口文件
@ComponentScan(basePackages = {"com.zjst.tc"},
excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Controller.class, EnableWebMvc.class})},
includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Service.class, Repository.class, Component.class})}
)
@PropertySource("classpath:/dbConfig.properties")
public class RootConfig implements EmbeddedValueResolverAware {
private StringValueResolver stringValueResolver;
/**
* mybatis配置
*
* @return
*/
@Bean
public SqlSessionFactoryBean sqlSessionFactoryBean() throws IOException {
ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();//mybatis-plus插件类
//配置数据源
sqlSessionFactoryBean.setDataSource(dataSource());
//配置mapper文件位置
sqlSessionFactoryBean.setMapperLocations(resourcePatternResolver.getResources("classpath:mappers/*.xml"));
//实体类所在的包
sqlSessionFactoryBean.setTypeAliasesPackage("com.zjst.tc.bean");
return sqlSessionFactoryBean;
}
/**
* 配置druid数据库连接池
* @return
*/
@Bean
public DataSource dataSource() {
com.alibaba.druid.pool.DruidDataSource dataSource = new DruidDataSource();
try {
dataSource.setUsername(stringValueResolver.resolveStringValue("${user}"));
dataSource.setPassword(stringValueResolver.resolveStringValue("${password}"));
dataSource.setUrl(stringValueResolver.resolveStringValue("${jdbcUrl}"));
dataSource.setDriverClassName(stringValueResolver.resolveStringValue("${driverClass}"));
dataSource.setInitialSize(Integer.parseInt(stringValueResolver.resolveStringValue("${initialSize}")));
dataSource.setMinIdle(Integer.parseInt(stringValueResolver.resolveStringValue("${minIdle}")));
dataSource.setMaxActive(Integer.parseInt(stringValueResolver.resolveStringValue("${maxActive}")));
dataSource.setMaxWait(Integer.parseInt(stringValueResolver.resolveStringValue("${maxWait}")));
dataSource.setTimeBetweenEvictionRunsMillis(Integer.parseInt(stringValueResolver.resolveStringValue("${timeBetweenEvictionRunsMillis}")));
dataSource.setMinEvictableIdleTimeMillis(Integer.parseInt(stringValueResolver.resolveStringValue("${minEvictableIdleTimeMillis}")));
dataSource.setValidationQuery(stringValueResolver.resolveStringValue("${validationQuery}"));
dataSource.setTestWhileIdle(Boolean.parseBoolean(stringValueResolver.resolveStringValue("${testWhileIdle}")));
dataSource.setTestOnBorrow(Boolean.parseBoolean(stringValueResolver.resolveStringValue("${testOnBorrow}")));
dataSource.setTestOnReturn(Boolean.parseBoolean(stringValueResolver.resolveStringValue("${testOnReturn}")));
dataSource.setPoolPreparedStatements(Boolean.parseBoolean(stringValueResolver.resolveStringValue("${poolPreparedStatements}")));
dataSource.setMaxPoolPreparedStatementPerConnectionSize(Integer.parseInt(stringValueResolver.resolveStringValue("${maxPoolPreparedStatementPerConnectionSize}")));
dataSource.setFilters(stringValueResolver.resolveStringValue("${filters}"));
dataSource.setConnectionProperties(stringValueResolver.resolveStringValue("${connectionProperties}"));
} catch (Exception e) {
System.out.println(e.getMessage());
}
return dataSource;
}
@Override
public void setEmbeddedValueResolver(StringValueResolver stringValueResolver) {
this.stringValueResolver = stringValueResolver;
}
}
3.4、利用 MyBatis Generator 自动生成接口文件和 xml 文件
暂时在test中添加 TestMybatisGenerator 类用于配置文件类
import org.mybatis.generator.api.MyBatisGenerator;
import org.mybatis.generator.config.*;
import org.mybatis.generator.internal.DefaultShellCallback;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class TestMybatisGenerator {
public static void main(String[] args) throws Exception {
Context context = new Context(ModelType.CONDITIONAL);
context.setTargetRuntime("MyBatis3");
context.setId("defaultContext");
//自动识别数据库关键字,默认false,如果设置为true,
//根据SqlReservedWords中定义的关键字列表;一般保留默认值,遇到数据库关键字(Java关键字),
//使用columnOverride覆盖
context.addProperty("autoDelimitKeywords", "true");
//生成的Java文件的编码
context.addProperty("javaFileEncoding", "utf-8");
context.addProperty("beginningDelimiter", "`");
context.addProperty("endingDelimiter", "`");
//格式化java代码
context.addProperty("javaFormatter", "org.mybatis.generator.api.dom.DefaultJavaFormatter");
//格式化xml代码
context.addProperty("xmlFormatter", "org.mybatis.generator.api.dom.DefaultXmlFormatter");
//格式化信息
PluginConfiguration pluginConfiguration = new PluginConfiguration();
pluginConfiguration.setConfigurationType("org.mybatis.generator.plugins.SerializablePlugin");
pluginConfiguration.setConfigurationType("org.mybatis.generator.plugins.ToStringPlugin");
context.addPluginConfiguration(pluginConfiguration);
//设置是否去除注释
CommentGeneratorConfiguration commentGeneratorConfiguration = new CommentGeneratorConfiguration();
commentGeneratorConfiguration.addProperty("suppressAllComments", "true");
//commentGeneratorConfiguration.addProperty("suppressDate","true");
context.setCommentGeneratorConfiguration(commentGeneratorConfiguration);
//设置连接数据库
JDBCConnectionConfiguration jdbcConnectionConfiguration = new JDBCConnectionConfiguration();
jdbcConnectionConfiguration.setDriverClass("com.mysql.cj.jdbc.Driver");
jdbcConnectionConfiguration.setConnectionURL("jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF8&serverTimezone=UTC&useSSL=false&nullCatalogMeansCurrent=true");
jdbcConnectionConfiguration.setPassword("1230");
jdbcConnectionConfiguration.setUserId("root");
context.setJdbcConnectionConfiguration(jdbcConnectionConfiguration);
JavaTypeResolverConfiguration javaTypeResolverConfiguration = new JavaTypeResolverConfiguration();
//是否使用bigDecimal, false可自动转化以下类型(Long, Integer, Short, etc.)
javaTypeResolverConfiguration.addProperty("forceBigDecimals", "false");
context.setJavaTypeResolverConfiguration(javaTypeResolverConfiguration);
//生成实体类(bean)的路径
JavaModelGeneratorConfiguration javaModelGeneratorConfiguration = new JavaModelGeneratorConfiguration();
javaModelGeneratorConfiguration.setTargetProject("src/main/java");
javaModelGeneratorConfiguration.setTargetPackage("com.zjst.tc.bean");
javaModelGeneratorConfiguration.addProperty("enableSubPackages", "true");
javaModelGeneratorConfiguration.addProperty("trimStrings", "true");
context.setJavaModelGeneratorConfiguration(javaModelGeneratorConfiguration);
//生成的xml的路径
SqlMapGeneratorConfiguration sqlMapGeneratorConfiguration = new SqlMapGeneratorConfiguration();
sqlMapGeneratorConfiguration.setTargetProject("src/main/resource");
sqlMapGeneratorConfiguration.setTargetPackage("mappers");
sqlMapGeneratorConfiguration.addProperty("enableSubPackages", "true");
context.setSqlMapGeneratorConfiguration(sqlMapGeneratorConfiguration);
//生成注解接口的路径
JavaClientGeneratorConfiguration javaClientGeneratorConfiguration = new JavaClientGeneratorConfiguration();
javaClientGeneratorConfiguration.setTargetProject("src/main/java");
javaClientGeneratorConfiguration.setTargetPackage("com.zjst.tc.repository");
//XMLMAPPER:sql写在xml中;ANNOTATEDMAPPER:sql写在注解中
javaClientGeneratorConfiguration.setConfigurationType("XMLMAPPER");
javaClientGeneratorConfiguration.addProperty("enableSubPackages", "true");
context.setJavaClientGeneratorConfiguration(javaClientGeneratorConfiguration);
//指定需要生成的表
TableConfiguration tableConfiguration = new TableConfiguration(context);
tableConfiguration.setTableName("role_permission");
tableConfiguration.setGeneratedKey(new GeneratedKey("id", "Mysql", true, null));
tableConfiguration.setCountByExampleStatementEnabled(true);
tableConfiguration.setUpdateByExampleStatementEnabled(true);
tableConfiguration.setDeleteByExampleStatementEnabled(true);
tableConfiguration.setInsertStatementEnabled(true);
tableConfiguration.setDeleteByPrimaryKeyStatementEnabled(true);
//Mysql无法正常支持SQL catalogs 和 schema
//tableConfiguration.setCatalog();
//tableConfiguration.setSchema("public");
context.addTableConfiguration(tableConfiguration);
List<String> warnings = new ArrayList<>();//用于装异常信息
MyBatisGenerator myBatisGenerator;
try {
Configuration config = new Configuration();
config.addContext(context);
config.validate();
boolean overwrite = true;
DefaultShellCallback callback = new DefaultShellCallback(overwrite);
myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
myBatisGenerator.generate(null);
} catch (Exception e) {
System.out.println("--->" + e.getMessage());
}
//若有异常警告,则在控制台打印
if (warnings.size() > 0) {
System.out.println("===>" + Arrays.asList(warnings));
}
}
}
此时会在指定的包中生成所需的文件
4、整合 SpringSecurity 登录验证和权限分配
4.1、将 MyBatis 自动生成 bean 改造
User 类实现 SpringSecurity 的 UserDetail 接口类,并添加 authorities 属性,用于装用户的角色 Role 对象,后续验证权限时使用(这个很重要)
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
public class User implements UserDetails, Serializable {
private Integer id;
private String username;
private String password;
/**
* 装用户的角色
*/
private List<Role> authorities;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username == null ? null : username.trim();
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password == null ? null : password.trim();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
public void setAuthorities(List<Role> authorities) {
this.authorities = authorities;
}
}
Role 类实现 SpringSecurity 的 GrantedAuthority 接口,重写 getAuthority 方法,返回 roleName(不是Role对象),用于后续的权限校验(这个很重要)
import org.springframework.security.core.GrantedAuthority;
public class Role implements GrantedAuthority {
private Integer id;
private String name;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String getAuthority() {
return name;
}
}
添加 RolePermissionDetail 类,用于装角色和权限的对应关系
import lombok.Data;
@Data
public class RolePermissionDetail {
private Integer roleId;
private String roleName;
private Integer permissionId;
private String permissionName;
private String permissionUrl;
}
4.2、创建 MyUserDetailsService 类
实现 SpringSecurity 的UserDetailsService 接口,重写 loadUserByUsername 方法,用于登录时,利用userName 获取用户的基本信息和角色信息(List),在用户请求某一受限路径的时候,用于匹配用户是否有足够的权限进行访问。
import com.mysql.cj.util.StringUtils;
import com.zjst.tc.bean.*;
import com.zjst.tc.repository.mappers.RoleMapper;
import com.zjst.tc.repository.mappers.UserMapper;
import com.zjst.tc.repository.mappers.UserRoleMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* 获取用户的信息和角色信息
*/
@Service("myUserDetailsService")
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleMapper roleMapper;
@Autowired
private UserRoleMapper userRoleMapper;
@Override
public UserDetails loadUserByUsername(String userName) {
if(StringUtils.isNullOrEmpty(userName)){
throw new UsernameNotFoundException("用户信息不能为空");
}
//获取用户信息
UserExample userExample = new UserExample();
UserExample.Criteria userCriteria = userExample.createCriteria();
userCriteria.andUsernameEqualTo(userName);
List<User> userList = userMapper.selectByExample(userExample);
User user = null;
if (userList.size() > 0) {
user = userList.get(0);
//获取用户与角色的关联信息
UserRoleExample userRoleExample = new UserRoleExample();
UserRoleExample.Criteria userRoleCriteria = userRoleExample.createCriteria();
userRoleCriteria.andUserIdEqualTo(user.getId());
List<UserRole> userRoleList = userRoleMapper.selectByExample(userRoleExample);
if (userRoleList.size() > 0) {
//获取用户的所有角色id
List<Integer> roleIdList = new ArrayList<>();
for (UserRole item : userRoleList) {
roleIdList.add(item.getRoleId());
}
//通过角色id获取角色信息
RoleExample roleExample = new RoleExample();
RoleExample.Criteria roleCriteria = roleExample.createCriteria();
roleCriteria.andIdIn(roleIdList);
List<Role> roleList = roleMapper.selectByExample(roleExample);
//将角色信息赋值给用户
user.setAuthorities(roleList);
}
}else {
throw new UsernameNotFoundException("用户不存在");
}
return user;
}
}
4.3、利用JavaConfig配置
SecurityAppInitializer
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
/**
* security的过滤器
*/
public class SecurityAppInitializer extends AbstractSecurityWebApplicationInitializer {
}
WebSecurityConfig(配置完成后,需要将其在2.4节的 WebAppInitializer 中添加)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Configuration;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.util.DigestUtils;
import javax.sql.DataSource;
@Configuration
@EnableWebSecurity//启用web安全性
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
@Qualifier("myUserDetailsService")
private UserDetailsService userDetailsService;
@Autowired
private MyFilterSecurityInterceptor myFilterSecurityInterceptor;
/**
* 自定义校验用户方法
*
* @param auth
* @throws Exception
*/
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
/**
* 方法一
* 将userDetailsService添加,用于自定义的用户登录校验
* 重写了密码加密后验证的方法(数据库中存储加密后的密码,但MD5加密已经不被SpringSecurity所推荐)
*/
auth.userDetailsService(userDetailsService).passwordEncoder(new PasswordEncoder() {
/**
* 对密码进行加密,md5加密
*/
@Override
public String encode(CharSequence charSequence) {
return DigestUtils.md5DigestAsHex(charSequence.toString().getBytes());
}
/**
* 对用户输入的密码进行匹配计算
*
* @param charSequence 用户输入的密码
* @param s 数据库中的用户密码
* @return
*/
@Override
public boolean matches(CharSequence charSequence, String s) {
String encode = encode(charSequence);
boolean res = s.equalsIgnoreCase(encode);
return res;
}
});
/**
* 方法二:
* 配置数据库连接,配置user-detail服务
* 重写usersByUsernameQuery和authoritiesByUsernameQuery,自定义校验
* BCryptPasswordEncoder加密验证(推荐之一)
* NoOpPasswordEncoder.getInstance()数据库明码存储密码(不推荐)
*/
// auth
// .jdbcAuthentication().dataSource(dataSource)
// .usersByUsernameQuery("select `name`, password, disable from users where `name` = ?")
// .authoritiesByUsernameQuery("" +
// "select a.`name` , c.`name`\n" +
// "from users as a \n" +
// "inner join user_role as b on a.id = b.user_id\n" +
// "inner join roles as c on c.id = b.role_id\n" +
// "where a.`name` = ?")
// .passwordEncoder(new BCryptPasswordEncoder());//BCrypt加密方法
.passwordEncoder(NoOpPasswordEncoder.getInstance());//不加密
}
/**
* 通过重载,配置SpringSecurity的Filter链
*
* @param web
*/
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers("/static/css/**");
web.ignoring().antMatchers("/static/js/**");
web.ignoring().antMatchers("/static/image/**");
//忽略登录界面
//web.ignoring().antMatchers("/login");
//注册地址不拦截
// web.ignoring().antMatchers("/reg");
}
/**
* 配置如何通过拦截器保护请求
*
* HttpSecurity用于提供一系列的Security默认的Filter,
* 这个过程中:
* 一是生成默认的FilterConfigurer对象并添加到其filters属性中存储 ;
* 二是调用其performBuild方法生成DefaultSecurityFilterChain对象;
* 在WebSecurityConfig中重载了WebSecurityConfigurerAdapter的configure方法,
* 利用HttpSercurity对象提供的一些filter去实现我们自己的业务
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().anyRequest().authenticated()
.and()
.formLogin().loginPage("/login").successForwardUrl("/main").failureUrl("/login-error").permitAll() // 登录
.and()
.logout().logoutSuccessUrl("/login") //登出
.and()
.exceptionHandling().accessDeniedPage("/401")// 权限不足,跳转401页面
.and()
.rememberMe().tokenValiditySeconds(1800)// 登录缓存时间
// 将自定义的权限过滤器添加至过滤器FilterSecurityInterceptor的前面
.and()
.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class);
//关闭csrf跨域攻击防护(开发时可关闭,但实际开发时不推荐)
// 关闭csrf后,可以通过get或post请求,同时不需要携带_csrf的token
// 当开启时,需要post请求,同时需要hidden隐藏域中添加_csrf的token
//.and()
//.csrf().disable();
}
}
public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[]{RootConfig.class, WebSecurityConfig.class};
}
//其他方法省略。。。
}
4.4、自定义拦截器 MyFilterSecurityInterceptor
实现 AbstractSecurityInterceptor 抽象类,实现 Filter 接口
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.SecurityMetadataSource;
import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
import org.springframework.security.access.intercept.InterceptorStatusToken;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import java.io.IOException;
/**
* 自定义拦截器
* 使用自定义的 MyAccessDecisionManager 和 MyFilterInvocationSecurityMetadataSource
* 登陆后,每次访问资源都会被这个Interceptor(拦截器)拦截,会执行doFilter,调用了invoke方法
*/
@Component
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
//注入自定义的验证资源提取类
@Autowired
@Qualifier("myFilterInvocationSecurityMetadataSource")
private FilterInvocationSecurityMetadataSource securityMetadataSource;
//提供获取自定义的验证资源获取类
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
//注入自定义的校验方法
@Autowired
@Qualifier("myAccessDecisionManager")
public void setMyAccessDecisionManager(AccessDecisionManager accessDecisionManager) {
super.setAccessDecisionManager(accessDecisionManager);
}
//认证管理器,实现用户认证的入口,使用父类的
@Override
public void setAuthenticationManager(AuthenticationManager newManager) {
super.setAuthenticationManager(newManager);
}
//在拦截器链中,会被执行,用于权限验证
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
FilterInvocation filterInvocation = new FilterInvocation(servletRequest, servletResponse, filterChain);
invoke(filterInvocation);
}
public void invoke(FilterInvocation fi) throws IOException, ServletException{
//beforeInvocation()方法实现了对访问受保护对象的权限校验,请求了MyAccessDecisionManager类的decide方法
// 内部用到了AccessDecisionManager和AuthenticationManager;
//如果请求者据有路径所需要的权限,则new了一个token对象--->InterceptorStatusToken
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
//执行下一个拦截器
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
//方法实现了对返回结果的处理,在注入了AfterInvocationManager的情况下默认会调用其decide()方法
//super:使用继承的抽象类AbstractSecurityInterceptor的afterInvocation方法
super.afterInvocation(token, null);
}
}
@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}
}
4.5、自定义资源获取类 MyFilterInvocationSecurityMetadataSource
用于所有的受限资源访问所需要的角色,在自定义拦截器 MyFilterSecurityInterceptor 的 invoke方法的 super.beforeInvocation(fi) 中使用,调用抽象类 AbstractSecurityInterceptor 的 beforeInvocation() 方法执行下述行,来获取所有资源所需要的角色。
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
import com.zjst.tc.bean.RolePermissionDetail;
import com.zjst.tc.repository.mappers.RolePermissionMapper;
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.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
/**
* 从数据库中提取权限资源,供Spring security使用,用于权限校验
*/
@Component("myFilterInvocationSecurityMetadataSource")
public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
private RolePermissionMapper rolePermissionMapper;
//每一个资源所需要的角色 Collection<ConfigAttribute>决策器会用到
//map的key存页面(资源)的url,value存所需要的角色(roles),可能有多个角色有权限访问该资源
private static HashMap<String, Collection<ConfigAttribute>> map = null;
/**
* 返回请求的资源需要的角色
*
* @param o 对象包含 servletRequest, servletResponse, filterChain
* @return
* @throws IllegalArgumentException
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
//减少请求数据库的次数,防止二次锁
if (map == null) {
synchronized (MyFilterInvocationSecurityMetadataSource.class) {
if (map == null) {
loadResourceDefine();
}
}
}
//object中包含 servletRequest, servletResponse, filterChain
HttpServletRequest request = ((FilterInvocation) o).getHttpRequest();
//将请求的url需要的角色返回
for (Map.Entry<String, Collection<ConfigAttribute>> item : map.entrySet()) {
if (new AntPathRequestMatcher(item.getKey()).matches(request)) {
return map.get(item.getKey());
}
}
return null;
}
/**
* Spring容器启动时自动调用, 一般把所有请求与权限的对应关系也要在这个方法里初始化, 保存在一个属性变量里
*
* @return
*/
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
/**
* 该类是否能够为指定的方法调用或Web请求提供ConfigAttributes
*
* @param aClass
* @return
*/
@Override
public boolean supports(Class<?> aClass) {
return true;
}
/**
* 初始化所有资源对应的角色
*/
private void loadResourceDefine() {
map = new HashMap<>();
List<RolePermissionDetail> rolePermissionDetailList = rolePermissionMapper.selectAll();
//某个资源 可以被哪些角色访问
for (RolePermissionDetail item : rolePermissionDetailList) {
String permissionUrl = item.getPermissionUrl();
String roleName = item.getRoleName();
ConfigAttribute role = new SecurityConfig(roleName);
if (map.containsKey(permissionUrl)) {
map.get(permissionUrl).add(role);
} else {
List<ConfigAttribute> ca = new ArrayList<>();
ca.add(role);
map.put(permissionUrl, ca);
}
}
}
}
4.6、自定义决策器 MyAccessDecisionManager
自定义决策器会在 AbstractSecurityInterceptor 的 beforeInvocation 方法的下述行中被执行:
this.accessDecisionManager.decide(authenticated, object, attributes);
有权限则 return,否则抛出异常 AccessDeniedException
import org.mybatis.logging.Logger;
import org.mybatis.logging.LoggerFactory;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import java.util.Collection;
/**
* 决策器
* 鉴定用户是否有访问对应资源(方法或URL)的权限
*/
@Component("myAccessDecisionManager")
public class MyAccessDecisionManager implements AccessDecisionManager {
//目前没有使用,可以将权限验证情况写入日志中
private final static Logger logger = LoggerFactory.getLogger(MyAccessDecisionManager.class);
/**
* 通过传递的参数来决定用户是否有访问对应受保护对象的权限
*
* @param authentication 包含了当前的用户信息,包括拥有的权限。
* 这里的权限来源就是前面登录时UserDetailsService中设置的authorities。
* SecurityContextHolder.getContext().getAuthentication();
* @param o 就是FilterInvocation对象,包含本次请求的 servletRequest, servletResponse, filterChain
* @param collection 本次访问需要的权限(角色list,存的是角色name)
*/
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
//不需要权限就可以访问
if (collection == null || collection.size() <= 0) {
return;
}
for (ConfigAttribute item : collection) {
//只需要请求者所据有的角色中,包含路径所需要的角色(权限)
//item.getAttribute()装的是请求路径所需要的roleName
//authentication.getAuthorities()请求者所据有的roles
if (authentication.getAuthorities().size() > 0) {
for (GrantedAuthority item2 : authentication.getAuthorities()) {
if (item2.getAuthority().contains(item.getAttribute())) {
return;
}
}
}
}
throw new AccessDeniedException("无权限访问");
}
/**
* 表示此AccessDecisionManager是否能够处理传递的ConfigAttribute呈现的授权请求
*
* @param configAttribute
* @return
*/
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
/**
* 表示当前AccessDecisionManager是否能够为指定的安全对象(方法调用或Web请求)提供访问控制决策
*
* @param aClass
* @return
*/
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
4.7、请求 SecurityController
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class SecurityController {
@RequestMapping({"/", "/index"})
public String root() {
return "redirect:/login";
}
//列表页面
@RequestMapping(value = "/main", method = RequestMethod.POST)
public String main() {
return "main";
}
@RequestMapping("/login")
public String login() {
//SpringSecurity框架进行校验
//通过后跳转至首页
return "login";
}
//若登录失败(用户名或密码不正确)
@RequestMapping("/login-error")
public String loginError(Model model) {
//model用于在login页面显示提示
model.addAttribute("loginError", true);
return "login";
}
//用于无权限访问提示页面
@GetMapping("/401")
public String accessDenied() {
return "user/401";
}
@GetMapping("/user/common")
//@PreAuthorize("hasRole('ROLE_COMMON')")
public String common() {
return "user/common";
}
@GetMapping("/user/admin")
//@PreAuthorize("hasRole('ROLE_ADMIN')")
public String admin() {
return "user/admin";
}
}
4.8、JSP
login.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="sf" uri="http://www.springframework.org/tags/form" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<h1>Login page</h1>
<c:if test="${loginError}">
<span style="color:red">提示:用户名或密码错误</span>
</c:if>
<sf:form action="/login" method="post">
<label for="username">用户名</label>:
<input type="text" id="username" name="username" autofocus="autofocus" />
<br/>
<label for="password">密 码</label>:
<input type="password" id="password" name="password" />
<br/>
<input type="submit" value="登录" />
</sf:form>
</body>
</html>
main.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
<%@ taglib prefix="sf" uri="http://www.springframework.org/tags/form" %>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<h2>page list</h2>
<a href="/user/common">common page</a>
<br/>
<a href="/user/admin">admin page</a>
<br/>
<br/>
需要将csrf的token利用post请求提交至后台,如果关闭csrf,则post和get均可
<form action="${pageContext.request.contextPath }/logout" method="post">
<input type="hidden" name="${_csrf.parameterName }" value="${_csrf.token }"/>
<input type="submit" value="logout">
</form>
<br/>
使用spring-form标签,可以自动添加_csrf的token
<sf:form action="/logout" method="post">
<input type="submit" value="logout">
</sf:form>
</body>
</html>
401.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>this is 401 page</h1>
<div>
<span>Insufficient permissions, access denied</span><br/>
<span>权限不足, 拒绝访问</span>
</div>
<br/>
为了防止csrf的跨域攻击,请求带token
<form action="/main" method="post">
<input type="hidden" name="${_csrf.parameterName }" value="${_csrf.token }"/>
<input type="submit" value="main">
</form>
</body>
</html>
admin.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>this is admin page</h1>
<br/>
为了防止csrf的跨域攻击,请求带token
<form action="/main" method="post">
<input type="hidden" name="${_csrf.parameterName }" value="${_csrf.token }"/>
<input type="submit" value="main返回列表页">
</form>
</body>
</html>
common.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>this is common page</h1>
<br/>
为了防止csrf的跨域攻击,请求带token
<form action="/main" method="post">
<input type="hidden" name="${_csrf.parameterName }" value="${_csrf.token }"/>
<input type="submit" value="main返回列表页">
</form>
</body>
</html>
5、最后贴上整个项目的构成图
6、参考文献(博文)
spring、springmvc和mybatis整合(java config方式)
https://www.cnblogs.com/hhhshct/p/9688079.html
----------------------------------------------------------------------------------------------
Spring Boot Security 详解
https://www.jianshu.com/p/e715cc993bf0
springMvc+spring security 注解方式实现权限控制
https://blog.csdn.net/camellia919/article/details/81945806
基于Spring Security实现权限管理系统
https://blog.csdn.net/cloume/article/details/83790111
Spring Security用户认证和权限控制(自定义实现)
https://blog.csdn.net/weixin_44516305/article/details/88868791
----------------------------------------------------------------------------------------------
SpringSecurity默认访问授权流程图
https://blog.csdn.net/weixin_33869377/article/details/92131050
SpringSecurity禁用匿名用户(anonymous().disable())后无限重定向到登录页的问题解决
http://blog.joylau.cn/2019/08/19/SpringBoot-SpringSecurity-Anonymous/
----------------------------------------------------------------------------------------------
SpringSecurity前后端分离下对登录认证的管理
https://blog.csdn.net/XlxfyzsFdblj/article/details/82083443
SpringSecurity登录验证步骤和源码浅析
https://blog.csdn.net/XlxfyzsFdblj/article/details/82084183#commentsedit
SpringSecurity自定义权限校验和源码浅析
https://blog.csdn.net/XlxfyzsFdblj/article/details/82262469
----------------------------------------------------------------------------------------------
SpringSecurity自定义异常处理
https://blog.csdn.net/XlxfyzsFdblj/article/details/82290500
----------------------------------------------------------------------------------------------
Spring Security 默认的过滤器链
https://blog.csdn.net/shenchaohao12321/article/details/87355803
spring security 标准Filter及其在filter chain的顺序
https://blog.csdn.net/shierqu/article/details/49538791
https://blog.csdn.net/selfsojourner/article/details/70891385