权限管理,一般指根据系统设置的安全规则或者安全策略,用户可以访问而且只能访问自己被授权的资源。权限管理几乎出现在任何系统里面,前提是需要有用户和密码认证的系统。
在权限管理的概念中,有两个非常重要的名词:
认证
:通过用户名和密码成功登陆系统后,让系统得到当前用户的角色身份。
授权
:系统根据当前用户的角色,给其授予对应可以操作的权限资源。
完成权限管理需要三个对象:
用户
:主要包含用户名,密码和当前用户的角色信息,可实现认证操作。角色
:主要包含角色名称,角色描述和当前角色拥有的权限信息,可实现授权操作。权限
:权限也可以称为菜单,主要包含当前权限名称,url地址等信息,可实现动态展示菜单。这也是经典的 RBAC 模式:
RBAC 是基于角色的访问控制(
Role-Based Access Control
)在 RBAC 中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予给角色,而把角色又赋予用户,这样的权限设计很清楚,管理起来很方便。
Spring Security
是 spring
采用AOP思想,基于 servlet
过滤器实现的安全框架。它提供了完善的认证机制
和方法级的授权
功能。是一款非常优秀的权限管理框架。
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-webmvcartifactId>
<version>5.1.5.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-webartifactId>
<version>5.1.5.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-configartifactId>
<version>5.1.5.RELEASEversion>
dependency>
<dependency>
<groupId>ch.qos.logbackgroupId>
<artifactId>logback-classicartifactId>
<version>1.2.3version>
dependency>
<dependency>
<groupId>javax.servletgroupId>
<artifactId>javax.servlet-apiartifactId>
<version>4.0.1version>
<scope>providedscope>
dependency>
<context:component-scan base-package="org.neuedu.security.demo.controller" />
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/pages/"/>
<property name="suffix" value=".jsp" />
bean>
<mvc:annotation-driven />
<mvc:default-servlet-handler />
<security:http >
<security:intercept-url pattern="/**" access="hasRole('ROLE_ADMIN')"/>
<security:form-login />
security:http>
<security:authentication-manager>
<security:authentication-provider>
<security:user-service>
<security:user name="user" password="{noop}user" authorities="ROLE_USER" />
<security:user name="admin" password="{noop}admin" authorities="ROLE_ADMIN" />
security:user-service>
security:authentication-provider>
security:authentication-manager>
auto-config="true"
自动配置,加上它,则可以省略 认证方式,即可以不用
use-expressions="true"
启用 SPEL 表达式,页面可以获取响应的认证对象
页面表单的形式认证
页面弹出框的形式认证
<filter>
<filter-name>springSecurityFilterChainfilter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxyfilter-class>
filter>
<filter-mapping>
<filter-name>springSecurityFilterChainfilter-name>
<url-pattern>/*url-pattern>
filter-mapping>
<servlet>
<servlet-name>springMVCservlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
<init-param>
<param-name>contextConfigLocationparam-name>
<param-value>classpath:application-*.xmlparam-value>
init-param>
<load-on-startup>1load-on-startup>
servlet>
<servlet-mapping>
<servlet-name>springMVCservlet-name>
<url-pattern>/url-pattern>
servlet-mapping>
index.jsp
<h1>SpringSecurity 权限管理 <a href="/logout">退出a>h1>
<p><a href="dept/add">添加部门a>p>
<p><a href="dept/del">删除部门a>p>
<p><a href="dept/edit">修改部门a>p>
<p><a href="dept/list">部门列表a>p>
运行可以看到,系统并没有进入我们期待的
index.jsp
页面,而是进入了一个登录页面,这个页面是由SpringSecurity
框架为我们提供的,我们自己配置了拦截所有资源,也就是说,所有资源请求都需要认证通过才能继续访问。而对应的用户名和密码就是我们自己配置的admin
与user
.在这个登录页面上输入用户名
user
,密码user
,点击Sign in
,这样就可以进入index.jsp
页面了。
到此,我们就完成了一个入门案例,但是我们在实际开发过程中,肯定不可能使用
SpringSecurity
提供的这个默认登录页面,不然不仅项目色调不一致,语言类型也不一致。
查看 SringSecurity
默认提供的登录界面,获取对应的默认数据: 请求方式
,字段名称
, 请求地址
…
在 SpringSecurity
主配置文件中指定认证页面配置信息,注意:登录页面需要放行,否则会出现死循环
<security:http pattern="/fail.jsp" security="none" />
<security:http auto-config="true" use-expressions="true">
<security:intercept-url pattern="/login.jsp" access="permitAll()" />
<security:intercept-url pattern="/dept/add" access="hasRole('ROLE_USER')" />
<security:intercept-url pattern="/dept/del" access="hasRole('ROLE_ADMIN')" />
<security:intercept-url pattern="/dept/edit" access="hasRole('ROLE_ADMIN')" />
<security:intercept-url pattern="/dept/list" access="hasAnyRole('ROLE_ADMIN','ROLE_USER')" />
<security:intercept-url pattern="/**" access="isFullyAuthenticated()" />
<security:form-login login-page="/login.jsp"
username-parameter="username"
password-parameter="password"
login-processing-url="/login"
default-target-url="/index.jsp"
authentication-failure-url="/fail.jsp" />
<security:logout logout-url="/logout" logout-success-url="/login.jsp" />
<security:csrf disabled="true" />
<security:access-denied-handler error-page="/fail.jsp" />
security:http>
<security:authentication-manager>
<security:authentication-provider>
<security:user-service>
<security:user name="user" password="{noop}user" authorities="ROLE_USER" />
<security:user name="admin" password="{noop}admin" authorities="ROLE_ADMIN" />
security:user-service>
security:authentication-provider>
security:authentication-manager>
表达式 = 描述
hasRole([role]) =当前用户是否拥有指定角色。
hasAnyRole([role1,role2]) =多个角色是一个以逗号进行分隔的字符串。如果当前用户拥有指定角色中的任 意一个则返回true。
hasAuthority([auth]) = 等同于hasRole
hasAnyAuthority([auth1,auth2]) =等同于hasAnyRole
Principle =代表当前用户的principle对象
authentication =直接从SecurityContext获取的当前Authentication对象
permitAll =总是返回true,表示允许所有的
denyAll =总是返回false,表示拒绝所有的
isAnonymous() =当前用户是否是一个匿名用户
isRememberMe() =表示当前用户是否是通过Remember-Me自动登录的
isAuthenticated() =表示当前用户是否已经登录认证成功了。
isFullyAuthenticated() =如果当前用户既不是一个匿名用户,同时又不是通过Remember-Me自动登录 的,则返回true。
403什么异常?这是
SpringSecurity
中的权限不足!这个异常怎么来的?还记得上面登录页面源码中的那个_csrf隐藏input吗?问题就在这了!
CSRF(Cross-site request forgery)跨站请求伪造,是一种难以防范的网络攻击方式。
SpringSecurity 的 csrf 机制把请求方式分成两类来处理 - 【 CsrfFilter 】。
第一类
:“GET
”, “HEAD
”, “TRACE
”, "OPTIONS
"四类请求可以直接通过.
第二类
:除去上面四类,包括POST
都要被验证携带token
或者 关闭csrf
防护才能通过.
SpringSecurity
主配置文件中添加禁用crsf防护的配置 :
token
请求: 导入 SpringSecurity
标签库,在表单中录入:
注意:一旦开启了csrf防护功能,logout处理器便只支持POST请求方式了!
- 客户端发起一个请求,进入 Security 过滤器链
- 当到 LogoutFilter 的时候判断是否是登出路径,如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理,如果登出失败则由 ExceptionTranslationFilter ;如果不是登出路径则直接进入下一个过滤器。
- 当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作,如果登录失败则到 AuthenticationFailureHandler 登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler 登录成功处理器处理,如果不是登录请求则不进入该过滤器。
- 当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到 Controller 层否则到 AccessDeniedHandler 鉴权失败处理器处理。
SecurityContextHolder
中,然后在该次请求处理完成之后,将 SecurityContextHolder
中关于这次请求的信息存储到一个“仓储”中,然后将 SecurityContextHolder
中的信息清除,例如在Session中维护一个用户的安全信息就是这个过滤器处理的。/login
且必须为 POST
请求, 默认使用的表单 name 值为 username
和 password
,这两个值可以通过设置这个过滤器的 usernameParameter
和 passwordParameter
两个参数的值进行修改。/logout
的请求,实现用户退出,清除认证信息。SecurityContextHolder
中认证信息为空,则会创建一个匿名用户存入到 SecurityContextHolder
中。SecurityContextHolder
中存储的用户信息来决定其是否有权限。remember me cookie
, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启
。Authentication 是一个接口,用来表示用户认证信息的,在用户登录认证之前相关信息会封装为一个 Authentication
具体实现类的对象,在登录认证成功之后又会生成一个信息更全面,包含用户权限等信息的 Authentication
对象,然后把它保存在 SecurityContextHolder
所持有的 SecurityContext
中,供后续的程序进行调用,如访问权限的鉴定等。
SecurityContextHolder 是用来保存 SecurityContext
的。SecurityContext
中含有当前正在访问系统的用户的详细信息。
AuthenticationManager 是一个用来处理认证(Authentication
)请求的接口。在其中只定义了一个方法 authenticate()
,该方法只接收一个代表认证请求的 Authentication
对象作为参数,如果认证成功,则会返回一个封装了当前用户权限等信息的 Authentication
对象进行返回。
校验认证请求最常用的方法是根据请求的用户名加载对应的 UserDetails
,然后比对 UserDetails
的密码与认证请求的密码是否一致,一致则表示认证通过。在认证成功以后会使用加载的 UserDetails
来封装要返回的 Authentication
对象,加载的 UserDetails
对象是包含用户权限等信息的。认证成功返回的 Authentication
对象将会保存在当前的 SecurityContext
中。
UserDetailsService 通过 Authentication.getPrincipal()
返回的其实是一个 UserDetails 实例
。UserDetails
是 Spring Security
中一个核心的接口。其中定义了一些可以获取用户名、密码、权限等与认证相关的信息的方法。Spring Security
内部使用的 UserDetails
实现类大都是内置的 User
类,我们如果要使用 UserDetails
时也可以直接使用该类。
UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext()
.getAuthentication().getPrincipal();
String username = userDetails.getUsername();
UsernamePasswordAuthenticationFilter
拦截认证, 请求必须是 POST
, 填写的用户名(username
)和密码(password
)会封装到 UsernamePasswordAuthenticationToken
中 , 调用 AuthenticationManager
对象实现认证. 实现类 AuthenticationProvider
完成认证业务,我们可以直接编写一个 UserDetailsService
的实现类返回一个UserDetails
对象即可。这里需要注意返回的对象中需要带有权限信息。
//自定义认证业务逻辑
public class UserService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//User(String username, String password, Collection extends GrantedAuthority> authorities)
//User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection extends GrantedAuthority> authorities)
List<GrantedAuthority> roles = new ArrayList<>();
roles.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
roles.add(new SimpleGrantedAuthority("ROLE_USER"));
//User user = new User("admin","{noop}admin",roles);
User user = new User("admin","{noop}admin",true,true,true,true,roles);
return user;
}
}
设置用户状态
在用户认证业务里,认证过程中,这四个参数必须同时为
true
认证才能通过,当然这四个字段我们也可以把它添加到数据库字段中,这样也可以完成动态设置.
boolean enabled
是否可用boolean accountNonExpired
账户是否失效boolean credentialsNonExpired
认证是否过期boolean accountNonLocked
账户是否锁定
上面的案例我们是自己模拟出一个用户信息和角色权限,接下来我们使用数据库中动态的数据来完成认证,其实很简单我们只需要在数据库中添加对应的用户表和角色表,然后添加两个方法 根据用户名查询用户信息
, 根据用户ID查询角色集合
, 然后动态的去替换上面 UserService
中的模拟数据即可。接下来我们开始搭建后端环境。
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>6.0.6version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plusartifactId>
<version>3.1.0version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.16.16version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-jdbcartifactId>
<version>5.1.5.RELEASEversion>
dependency>
<dependency>
<groupId>ch.qos.logbackgroupId>
<artifactId>logback-classicartifactId>
<version>1.2.3version>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-testartifactId>
<version>5.1.5.RELEASEversion>
dependency>
<dependency>
<groupId>com.fasterxml.jackson.coregroupId>
<artifactId>jackson-databindartifactId>
<version>2.9.6version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.1.14version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-generatorartifactId>
<version>3.1.0version>
dependency>
<dependency>
<groupId>org.freemarkergroupId>
<artifactId>freemarkerartifactId>
<version>2.3.28version>
dependency>
RBAC 数据表结构
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for sys_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_permission`;
CREATE TABLE `sys_permission` (
`ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号',
`permission_NAME` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '菜单名称',
`permission_url` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '菜单地址',
`parent_id` int(11) NOT NULL DEFAULT 0 COMMENT '父菜单id',
PRIMARY KEY (`ID`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of sys_permission
-- ----------------------------
INSERT INTO `sys_permission` VALUES (1, '部门添加 ', '/dept/add', 0);
INSERT INTO `sys_permission` VALUES (2, '部门删除', '/dept/del', 0);
INSERT INTO `sys_permission` VALUES (3, '部门修改', '/dept/edit', 0);
INSERT INTO `sys_permission` VALUES (4, '部门列表', '/dept/list', 0);
-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号',
`ROLE_NAME` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '角色名称',
`ROLE_DESC` varchar(60) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '角色描述',
PRIMARY KEY (`ID`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES (1, 'ROLE_ADMIN', '管理员角色');
INSERT INTO `sys_role` VALUES (2, 'ROLE_USER', '普通用户角色');
-- ----------------------------
-- Table structure for sys_role_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_permission`;
CREATE TABLE `sys_role_permission` (
`RID` int(11) NOT NULL COMMENT '角色编号',
`PID` int(11) NOT NULL COMMENT '权限编号',
PRIMARY KEY (`RID`, `PID`) USING BTREE,
INDEX `FK_Reference_12`(`PID`) USING BTREE,
CONSTRAINT `FK_Reference_11` FOREIGN KEY (`RID`) REFERENCES `sys_role` (`ID`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `FK_Reference_12` FOREIGN KEY (`PID`) REFERENCES `sys_permission` (`ID`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of sys_role_permission
-- ----------------------------
INSERT INTO `sys_role_permission` VALUES (1, 1);
INSERT INTO `sys_role_permission` VALUES (1, 2);
INSERT INTO `sys_role_permission` VALUES (2, 3);
INSERT INTO `sys_role_permission` VALUES (2, 4);
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户名称',
`password` varchar(120) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '密码',
`status` int(1) DEFAULT 1 COMMENT '1开启0关闭',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (1, 'user', '{noop}user', 1);
INSERT INTO `sys_user` VALUES (2, 'admin', '{noop}admin', 1);
-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`UID` int(11) NOT NULL COMMENT '用户编号',
`RID` int(11) NOT NULL COMMENT '角色编号',
PRIMARY KEY (`UID`, `RID`) USING BTREE,
INDEX `FK_Reference_10`(`RID`) USING BTREE,
CONSTRAINT `FK_Reference_10` FOREIGN KEY (`RID`) REFERENCES `sys_role` (`ID`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `FK_Reference_9` FOREIGN KEY (`UID`) REFERENCES `sys_user` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
INSERT INTO `sys_user_role` VALUES (2, 1);
INSERT INTO `sys_user_role` VALUES (1, 2);
INSERT INTO `sys_user_role` VALUES (2, 2);
MP 代码生成器
//读取属性配置文件
private ResourceBundle rb = ResourceBundle.getBundle("druid");
@Test
public void codeGenerator(){
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/src/main/java");
gc.setAuthor("CDHong");
gc.setOpen(false);
gc.setSwagger2(false); //实体属性 Swagger2 注解
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl(rb.getString("url"));
// dsc.setSchemaName("public");
dsc.setDriverName(rb.getString("driver"));
dsc.setUsername(rb.getString("user"));
dsc.setPassword(rb.getString("pwd"));
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
//父级公用包名,就是自动生成的文件放在项目路径下的那个包中
pc.setParent("org.neuedu.spring.security.demo");
mpg.setPackageInfo(pc);
// 自定义配置
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
// to do nothing
}
};
String templatePath = "/templates/mapper.xml.ftl";
// 自定义输出配置
List<FileOutConfig> focList = new ArrayList<>();
// 自定义配置会被优先输出
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
// 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
return projectPath + "/src/main/resources/mappers/" +
tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
}
});
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);
// 配置模板
TemplateConfig templateConfig = new TemplateConfig();
templateConfig.setXml(null); //是否在mapper接口处生成xml文件
mpg.setTemplate(templateConfig); //设置模板引擎
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
// 策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setNaming(NamingStrategy.underline_to_camel); //Entity文件名称命名规范
strategy.setColumnNaming(NamingStrategy.underline_to_camel);//Entity字段名称命名规范
strategy.setEntityLombokModel(true); //是否使用lombok完成Entity实体标注
strategy.setRestControllerStyle(true); //Controller注解使用是否RestController标注
strategy.setControllerMappingHyphenStyle(true); //Controller注解名称,使用连字符(—)
strategy.setInclude("sys_user","sys_role","sys_permission"); //要生成的表名,不写默认所有
//strategy.setTablePrefix("sys_");//表前缀,添加该表示,则生成的实体,不会有表前缀
//strategy.setFieldPrefix("sys_"); //字段前缀
mpg.setStrategy(strategy);
mpg.execute();
}
SpringSecurity + MP 运行环境
<context:property-placeholder location="classpath:druid.properties" />
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
<property name="driverClassName" value="${driver}" />
<property name="url" value="${url}" />
<property name="username" value="${user}" />
<property name="password" value="${pwd}" />
<property name="initialSize" value="${initSize}" />
<property name="maxWait" value="${maxWait}" />
<property name="maxActive" value="${maxSize}" />
<property name="minIdle" value="${minSize}" />
bean>
<bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean">
<property name="dataSource" ref="druidDataSource" />
<property name="typeAliasesPackage" value="org.neuedu.spring.security.demo.entity" />
<property name="mapperLocations" value="classpath:mappers/*Mapper.xml" />
<property name="plugins">
<bean class="com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor" />
property>
bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="org.neuedu.spring.security.demo.mapper" />
bean>
<context:component-scan base-package="org.neuedu.spring.security.demo.service.impl" />
测试数据录入
在 controller
中添加一个 list
请求方法,测试整合端是否 OK。
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResponseEntity {
private int code;
private String msg;
private Long count;
private Object data;
public static ResponseEntity ok(){
return new ResponseEntity(0,null,null,null);
}
public static ResponseEntity ok(String msg){
return new ResponseEntity(0,msg,null,null);
}
public static ResponseEntity error(String msg){
return new ResponseEntity(1,msg,null,null);
}
public static ResponseEntity data(Object obj){
return new ResponseEntity(0,null,null,obj);
}
public static ResponseEntity page(long count,Object obj){
return new ResponseEntity(0,null,count,obj);
}
public static boolean isSuccess(ResponseEntity responseEntity){
return responseEntity.getCode() == 1;
}
}
在角色Mapper中添加一个查询角色的方法
public interface SysRoleMapper extends BaseMapper<SysRole> {
@Select(" select r.id,r.role_name,r.role_desc from sys_user u " +
" join sys_user_role ur on u.id = ur.UID " +
" join sys_role r on r.id = ur.RID " +
" where u.id = #{userId} ")
List<SysRole> findRoldByUserId(Integer userId);
}
动态权限认证更改
修改 SysUserServiceImp
类,实现UserDetailsService
接口,重写 loadUserByUsername
方法,完成认证逻辑,动态替换认证数据:
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements ISysUserService, UserDetailsService {
@Autowired
private SysRoleMapper roleMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名获取对应的用户信息 User
SysUser user = new LambdaQueryChainWrapper<SysUser>(baseMapper).eq(SysUser::getUsername,username).one();
if(Objects.isNull(user)){
return null;
}
//获取权限集合 SimpleGrantedAuthority
List<SysRole> roles = roleMapper.findRoldByUserId(user.getId());
//User
Set<SimpleGrantedAuthority> authorities = new HashSet<>();
roles.forEach(role-> authorities.add(new SimpleGrantedAuthority(role.getRoleName())));
return new User(user.getUsername(),user.getPassword(),user.getStatus()==1,true,true,true,authorities);
}
}
修改
application-security.xml
中的认证逻辑引用
<security:authentication-manager>
<security:authentication-provider user-service-ref="sysUserServiceImpl" />
security:authentication-manager>
到此完毕,但是,在认证逻辑中我们都是手动组装数据,这样比较麻烦,有没有办法可以简化呢?
接下来,我们只需要把
SysUser
类变成UserDetails
的子类 , 把SysRole
类变成GrantedAuthority
的子类是不是就可以了呢?
数据库实体关系
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("sys_role")
public class Role implements Serializable, GrantedAuthority {
private static final long serialVersionUID=1L;
// 编号
@TableId(value = "ID", type = IdType.AUTO)
private Integer id;
// 角色名称
@TableField("ROLE_NAME")
private String roleName;
//角色描述
@TableField("ROLE_DESC")
private String roleDesc;
@Override
public String getAuthority() {
//返回用于认证的角色描述信息 ROLE_ADMIN,ROLE_USER 这类用于判断的对应字段
return this.roleName;
}
}
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("sys_user")
public class User implements Serializable, UserDetails {
private static final long serialVersionUID=1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
//用户名称
private String username;
//密码
private String password;
//1开启0关闭
private Integer status;
//拥有的所有角色
@TableField(exist = false) //数据库中不存在该字段,使用注解排除
private List<Role> roles = new ArrayList<>();
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//返回当前用户认证角色集合
return roles;
}
//账户是否失效
@Override
public boolean isAccountNonExpired() {return true;}
//账户是否锁定
@Override
public boolean isAccountNonLocked() {return true;}
//认证是否过期
@Override
public boolean isCredentialsNonExpired() {return true;}
//是否可用,使用数据库的用户状态来进行动态处理
@Override
public boolean isEnabled() {return this.getStatus()==1;}
}
数据库 RoleMapper 认证方法实现
public interface RoleMapper extends BaseMapper<Role> {
@Select("SELECT r.ID,r.ROLE_NAME,r.ROLE_DESC FROM sys_user u " +
" join sys_user_role ur on u.id = ur.UID " +
" join sys_role r on r.ID = ur.RID " +
" where u.id = #{userId} ")
List<Role> findByUserId(Integer userId);
}
public interface UserMapper extends BaseMapper<User> {
//注意:一定要提供一个 coloum = "查询字段" , 否则 MP 会直接去找属性叫 roles 的字段,直接报错
@Results({
@Result(id = true,property = "id",column = "id"),
@Result(property = "roles",column = "id",many = @Many(select = "org.neuedu.security.demo.mapper.RoleMapper.findByUserId"))
})
@Select("select u.id,u.username,u.password,u.status from sys_user u where u.username = #{username} ")
User findByName(String username);
}
具体认证逻辑实现
@Slf4j
@Service("userService")
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService, UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = baseMapper.findByName(username);
log.info("登录用户信息:{}",user);
log.info("登录用户拥有的认证角色:{}",user.getAuthorities());
return user;
}
}
指定认证使用的业务对象
<security:authentication-manager>
<security:authentication-provider user-service-ref="userService">
security:authentication-provider>
security:authentication-manager>
到此,大功告成,可以到页面是测试数据库动态权限,是否OK。
SpringSecurity 提供了很多种密码加密的形式,而我们之前为了简单,我们使用了明文不加密的形式登录,现在我们来看看它具体的加密形式怎么使用,先修改数据库中用户的密码,去掉 {noop}
改成指定加密方式的密码:
<bean id="passwordEncoder"
class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
<security:authentication-manager>
<security:authentication-provider user-service-ref="userServiceImpl">
<security:password-encoder ref="passwordEncoder"/>
security:authentication-provider>
security:authentication-manager>
这里去掉
{noop}
,密码需要加密后在入库,否则密码不匹配。
@Test
public void test(){
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String admin = passwordEncoder.encode("admin");
String user = passwordEncoder.encode("user");
System.out.println(admin);
//$2a$10$GWHwW0.oU1FRlyAgR3ZMyuE1SlRlzPMfYktNG56n4oPnQCmm9Rpg.
System.out.println(user);
//$2a$10$SpT/iNJh3jYTbTFZEpXl8OFd60Hc18SmRU7OmwhWh93CdvvIW2G4C
}
remember me
功能,到 AbstractRememberMeServices
类中查看 loginSuccess
方法:登录的时候我们传递一个参数 remember-me
,如果它的值为 true
,on
,yes
,1
其中一个,则表示页面勾选了记住我选项了。具体业务逻辑由 PersistentTokenBasedRememberMeServices
完成。在这里还得注意需要开启记住我
功能的过滤器。注意:验证方式不能使用 isFullyAuthenticated()
, 否则记住我这个功能无法成功。
<security:http auto-config="true" use-expressions="true">
<security:remember-me data-source-ref="dataSource"
token-validity-seconds="60"
remember-me-parameter="remember-me"/>
security:http>
CREATE TABLE `persistent_logins` (
`username` varchar(64) NOT NULL,
`series` varchar(64) NOT NULL,
`token` varchar(64) NOT NULL,
`last_used` timestamp NOT NULL,
PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
注意这张表的名称和字段都是固定的,不要修改。在我们完成认证的时候,该数据库会有相应的记录来存储记住我的 cookie 值
<security:remember-me token-validity-seconds="600" token-repository-ref="jdbcTokenRepository" /> <bean id="jdbcTokenRepository" class="org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl"> <property name="dataSource" ref="druidDataSource" /> <property name="createTableOnStartup" value="true" /> bean>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
控制每一个前端请求的权限,配置如下:
<security:http pattern="/favicon.ico" security="none" />
<security:http pattern="/failure.jsp" security="none" />
<security:http pattern="/login.jsp" security="none" />
<security:http auto-config="true" use-expressions="true">
<security:intercept-url pattern="/dept/add" access="hasRole('ROLE_ADMIN')" />
<security:intercept-url pattern="/dept/del" access="hasRole('ROLE_ADMIN')" />
<security:intercept-url pattern="/dept/edit" access="hasRole('ROLE_USER')" />
<security:intercept-url pattern="/dept/list" access="hasAnyRole('ROLE_USER','ROLE_ADMIN')" />
<security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER','ROLE_ADMIN')" />
Spring Security
也有对 Jsp
标签支持的标签库。其中一共定义了三个标签:authorize
、authentication
和accesscontrollist
(不常用)。其中 authentication
标签是用来代表当前 Authentication
对象的,我们可以利用它来展示当前 Authentication
对象的相关信息。另外两个标签是用于权限控制的,可以利用它们来包裹需要保护的内容,通常是超链接和按钮。
authorize
: 是用来判断普通权限的,通过判断用户是否具有对应的权限而控制其所包含内容的显示,其可以指定如下属性。
access
: 需要使用表达式来判断权限,当表达式的返回结果为true时表示拥有对应的权限ifAllGranted
: 不能使用表达式,由逗号分隔的权限列表,用户必须拥有所有列出的权限时显示ifAnyGranted
: 不能使用表达式,用户必须至少拥有其中的一个权限时才能显示ifNotGranted
: 不能使用表达式,用户未拥有所有列出的权限时才能显示authentication
: 用来代表当前 Authentication
对象,主要用于获取当前 Authentication
的相关信息
property
: 主要属性,我们可以通过它来获取当前 Authentication
对象的相关信息scope
: 定义var存在的范围,默认是 pageContext
var
: 定义一个变量 , 将其以指定的属性名进行存放,默认是存放在 pageConext
中htmlScape
: 是否需要将 html
进行转义。默认为 true
。<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-taglibsartifactId>
<version>5.1.5.RELEASEversion>
dependency>
<%@taglib uri="http://www.springframework.org/security/tags" prefix="security" %>
<security:authorize access="hasAnyRole('ROLE_ADMIN')">
<p><a href="dept/add">部门添加a>p>
security:authorize>
<sec:authorize ifAllGranted="ROLE_ADMIN">
<a href="admin.jsp">admina>
sec:authorize>
<sec:authorize ifAnyGranted="ROLE_USER,ROLE_ADMIN">hellosec:authorize>
<sec:authorize ifNotGranted="ROLE_ADMIN">
<a href="user.jsp">usera>
sec:authorize>
欢迎你:<security:authentication property="principal.username" />
或者
欢迎你:<security:authentication property="name" />
SpringSecurity
可以通过注解的方式来控制类或者方法的访问权限。注解需要对应的注解支持,若注解放在
controller
类中,对应注解支持应该放在 mvc
配置文件中,因为 controller
类是有 mvc
配置文件扫描并创建的,同理,注解放在 service
类中,对应注解支持应该放在 spring
配置文件中。由于我们现在是模拟业务操作,并没有 service
业务代码,所以就把注解放在 controller
类中了。
在服务的我们可以通过 SpringSecurity
提供的注解对方法来进行权限控制,SpringSecurity
在方法的权限控制上支持三种注解: JSR-250注解
, @Secured注解
, 支持表达式的注解
。这三种注解默认是没有开启的,需要单独通过 global-method-security
元素对应的属性进行启用
<security:global-method-security jsr250-annotations="enabled" />
<security:global-method-security secured-annotations="enabled" />
<security:global-method-security pre-post-annotations="enabled" />
也可通过注解开启 , 需要在继承
WebSecurityConfigurerAdapter
类上加@EnableGlobalMethodSecurity
注解,并在该类中将AthenticationManager
定义为Bean
.
@RolesAllowed({"USER","ADMIN"})
具有两种权限中的一种,就可以访问。这里可以省略前缀 ROLE_
@PermitAll
表示允许所有的角色进行访问,也就是说不进行权限控制
@DenyAll
表示无论什么角色都不可以访问,与 @PermitAll
相反
与 JSR-250注解
使用一致,只是这个注解是 SpringSecurity
默认提供的,使用的时候不用额外引入 坐标 ,还有一点就是这个注解的角色需要加上前缀 ROLE_
SPEL
表达式的注解@PreAuthorize("hasRole('ADMIN')")
在方法调用之前,基于表达式的计算结果来限制对方法的访问@PostAuthorize
允许方法调用,但是如果表达式计算结果为 false
,将抛出一个安全性异常@PostFilter
允许方法调用,但必须按照表达式来过滤方法的结果@PreFilter
允许方法调用,但必须在进入方法之前过滤输入值
<security:global-method-security pre-post-annotations="enabled" />
@RestController
@RequestMapping("/dept")
public class DeptController {
@Autowired
private ISysUserService userService;
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/add")
public String add(){
return "dept add ..... ";
}
@PreAuthorize("hasRole('ROLE_ADMIN') and #id==5 ")
@GetMapping("/del")
public String del(Integer id){
return id+"dept del ..... ";
}
@PreAuthorize("hasRole('ROLE_USER')")
@GetMapping("/edit")
public String edit(){
return "dept edit ..... ";
}
@PostAuthorize("returnObject.username.equals('admin')")
@GetMapping("/list")
public SysUser list(Integer id){
return userService.getById(id);
}
}
@ControllerAdvice
public class ControllerExceptionAdvice {
//只有出现AccessDeniedException异常才调转403.jsp页面
@ExceptionHandler(AccessDeniedException.class)
public String exceptionAdvice(){
return "forward:/403.jsp";
}
}
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.1.3.RELEASEversion>
<relativePath/>
parent>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
dependencies>
@RestController
@RequestMapping("/product")
public class ProductController {
@RequestMapping
public String hello(){
return "success";
}
}
SpringBoot
已经为SpringSecurity
提供了默认配置,默认所有资源都必须认证通过才能访问,那么问题来了!此刻并没有连接数据库,也并未在内存中指定认证用户,如何认证呢?其实SpringBoot已经提供了默认用户名 user ,密码在项目启动时随机生成,在日志中可以查看到:
SpringBoot 官方是不推荐在 SpringBoot 中使用 jsp 的,那么到底可以使用吗?答案是肯定的!
不过需要导入 tomcat 插件启动项目,不能再用 SpringBoot 默认 tomcat 了。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-tomcatartifactId>
dependency>
<dependency>
<groupId>org.apache.tomcat.embedgroupId>
<artifactId>tomcat-embed-jasperartifactId>
dependency>
spring.mvc.view.prefix=/pages/
spring.mvc.view.suffix=.jsp
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//先不连接数据库,提供静态用户名和密码
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user")
.password("{noop}123")
.roles("USER");
}
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login.jsp", "/failer.jsp", "/css/**", "/img/**",
"/plugins/**").permitAll()
.antMatchers("/**").hasAnyRole("USER")
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginPage("/login.jsp")
.loginProcessingUrl("/login")
.successForwardUrl("/index.jsp")
.failureForwardUrl("/failer.jsp")
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.invalidateHttpSession(true)
.logoutSuccessUrl("/login.jsp")
.permitAll()
.and()
.csrf()
.disable();
}
}
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.47version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.2.0version>
dependency>
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql:///security_authority
spring.datasource.username=root
spring.datasource.password=root
logging.level.org.neuedu=debug
创建角色 pojo 对象 - 这里直接使用
SpringSecurity
的角色规范
@Data
public class SysRole implements GrantedAuthority {
private Integer id;
private String roleName;
private String roleDesc;
//标记此属性不做json处理
@JsonIgnore
@Override
public String getAuthority() {
return roleName;
}
}
创建用户 pojo 对象,这里直接实现
SpringSecurity
的用户对象接口,并添加角色集合私有属性。
@Data
public class SysUser implements UserDetails {
private Integer id;
private String username;
private String password;
private Integer status;
private List<SysRole> roles = new ArrayList<>();
@JsonIgnore
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles;
}
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true;
}
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return true;
}
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@JsonIgnore
@Override
public boolean isEnabled() {
return true;
}
}
提供角色 mapper 接口
public interface RoleMapper extends Mapper<SysRole> {
@Select("SELECT r.id, r.role_name roleName, r.role_desc roleDesc " +
"FROM sys_role r, sys_user_role ur " +
"WHERE r.id=ur.rid AND ur.uid=#{uid}")
public List<SysRole> findByUid(Integer uid);
}
提供用户mapper接口
public interface UserMapper extends Mapper<SysUser> {
@Select("select * from sys_user where username=#{username}")
@Results({
@Result(id = true, property = "id", column = "id"),
@Result(property = "roles", column = "id", javaType = List.class,
many = @Many(select = "com.itheima.mapper.RoleMapper.findByUid"))
})
public SysUser findByUsername(String username);
}
提供认证 service 接口
import org.springframework.security.core.userdetails.UserDetailsService;
public interface UserService extends UserDetailsService {
}
提供认证service实现类
@Service
@Transactional
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
return userMapper.findByUsername(s);
}
}
启动类中加入 Mapper 接口扫描 以及 密码加密 Bean 对象
@SpringBootApplication
@MapperScan("org.neuedu.security.mapper")
public class Application {
public static void main(String[] args) {
SpringApplication.run(QuickStartApplication.class, args);
}
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
修改配置类
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Autowired
private BCryptPasswordEncoder passwordEncoder;
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder);
}
protected void configure(HttpSecurity http) throws Exception {
http
允许不登陆就可以访问的方法,多个用逗号分隔
.authorizeRequests()
.antMatchers("/login.jsp", "/failer.jsp", "/css/**", "/img/**",
"/plugins/**").permitAll()
.antMatchers("/**").hasAnyRole("USER")
//其他的需要授权后访问
.anyRequest()
.authenticated()
.and()
//表单登录
.formLogin()
.loginPage("/login.jsp")
.loginProcessingUrl("/login")
.successForwardUrl("/index.jsp")
.failureForwardUrl("/failer.jsp")
.permitAll()
.and()
//退出
.logout()
.logoutUrl("/logout")
.invalidateHttpSession(true)
.logoutSuccessUrl("/login.jsp")
.permitAll()
.and()
//关闭 csrf
.csrf()
.disable();
}
}
在启动类上添加开启方法级的授权注解 @EnableGlobalMethodSecurity(prePostEnabled= true)
在产品处理器类上添加注解 @PreAuthorize('ADMIN')
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/test")
public String test(){
return "info";
}
编写异常处理器拦截403异常
@ControllerAdvice
public class HandleControllerException {
@ExceptionHandler(RuntimeException.class)
public String exceptionHandler(RuntimeException e){
if(e instanceof AccessDeniedException){
//如果是权限不足异常,则跳转到权限不足页面!
return "redirect:/403.jsp";
}
//其余的异常都到500页面!
return "redirect:/500.jsp";
}
}
使用默认用户名 user 与 控制台生成的随机密码进行登录
#修改配置文件,自定义用户名和密码
spring.security.user.name=admin
spring.security.user.password=admin
//继承 WebSecurityConfigurerAdapter 重写 configure(auth) 方法,代码指定用户名和密码
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//认证相关 用户名密码
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//基于内存
auth.inMemoryAuthentication()
.withUser("admin").password("{noop}admin").roles("ROLE_ADMIN")
.and()
.withUser("user").password("{noop}user").roles("ROLE_USER");
//基于数据库
}
}
//继承 WebSecurityConfigurerAdapter 重写 configure(http) 方法,完成授权操作
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/hello").hasAnyRole("ADMIN")
.anyRequest().authenticated();
//表单登录
http.formLogin()
}
//授权,请求相关
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/logPage").permitAll()
.antMatchers("/hello").hasAnyRole("ADMIN")
.anyRequest().authenticated();
//表单的配置
http.formLogin()
.usernameParameter("logName").passwordParameter("logPwd")
.loginPage("/logPage").loginProcessingUrl("/login");
//csrf
http.csrf().disable();
}
//授权,请求相关
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/logPage").permitAll()
.antMatchers("/hello").hasAnyRole("ADMIN")
.anyRequest().authenticated();
//表单的配置
http.formLogin()
.usernameParameter("logName").passwordParameter("logPwd")
.loginPage("/logPage").loginProcessingUrl("/login");
//记住我
http.rememberMe().rememberMeParameter("forgetMe").rememberMeCookieName("forgetMe")
.tokenValiditySeconds(5);
//csrf
http.csrf().disable();
}
- 角色名称需要注意:加上
ROLE_
前缀,做判断的时候,可以不用省略- 用户登录密码:如果是明文登录需要加上
{noop}
前缀,否则需要生成加密密码在存储到数据表中
-- ----------------------------
-- Table structure for sys_authority
-- ----------------------------
DROP TABLE IF EXISTS `sys_authority`;
CREATE TABLE `sys_authority` (
`id` int(11) NOT NULL COMMENT '主键编号',
`authority_name` varchar(25) COMMENT '权限名称',
`authority_url` varchar(25) COMMENT '权限地址',
`icon` varchar(25) COMMENT '图标',
`parent_id` int(11) DEFAULT NULL COMMENT '上级模块',
`permission` varchar(255) COMMENT '权限值',
`sort_num` int(3) DEFAULT NULL COMMENT '排序号',
`remark` varchar(200) COMMENT '备注',
`create_time` timestamp DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
PRIMARY KEY (`id`) USING BTREE
) COMMENT = '权限';
-- ----------------------------
-- Records of sys_authority
-- ----------------------------
INSERT INTO `sys_authority` VALUES (1, '用户管理', '/user/list', NULL, NULL, 'user:add,user:del', NULL, NULL, '2020-01-03 14:14:06');
-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键编号',
`role_name` varchar(25) COMMENT '角色名称',
`remark` varchar(200) COMMENT '备注',
`create_time` timestamp DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
PRIMARY KEY (`id`) USING BTREE
) COMMENT = '角色';
-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES (1, 'ROLE_ADMIN', '系统管理员', '2020-01-03 14:12:47');
INSERT INTO `sys_role` VALUES (2, 'ROLE_MGR', '销售主管', '2020-01-03 14:12:51');
-- ----------------------------
-- Table structure for sys_role_authority
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_authority`;
CREATE TABLE `sys_role_authority` (
`id` int(11) NOT NULL COMMENT '主键编号',
`authority_id` int(11) DEFAULT NULL COMMENT '权限ID',
`role_id` int(11) DEFAULT NULL COMMENT '角色ID',
`remark` varchar(200) COMMENT '备注',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
PRIMARY KEY (`id`) USING BTREE
) COMMENT = '角色-权限关系表';
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键编号',
`real_name` varchar(25) COMMENT '真实姓名',
`log_name` varchar(25) COMMENT '登录名',
`log_pwd` varchar(64) COMMENT '密码',
`gender` int(12) DEFAULT NULL COMMENT '性别,0 女,1男',
`disabled` int(255) DEFAULT 1 COMMENT '是否禁用,1启用,0禁用',
`remark` varchar(200) COMMENT '备注',
`create_time` timestamp DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
PRIMARY KEY (`id`) USING BTREE
) COMMENT = '用户';
-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (1, '刘颖', 'admin', 'admin', 0, 1, NULL, '2020-01-03 14:11:32');
-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键编号',
`user_id` int(11) DEFAULT NULL COMMENT '用户ID',
`role_id` int(11) DEFAULT NULL COMMENT '角色ID',
`remark` varchar(200) COMMENT '备注',
`create_time` timestamp DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
PRIMARY KEY (`id`)
) COMMENT = '用户角色关系表';
-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
INSERT INTO `sys_user_role` VALUES (1, 1, 1, NULL, '2019-12-14 15:14:20');
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql:///crm_system?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.druid.initial-size=10
spring.datasource.druid.max-active=50
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.default-property-inclusion=non_null
@Configuration
public class LocalDateTimeSerializerConfig {
@Value("${spring.jackson.date-format}")
private String pattern;
@Bean
public LocalDateTimeSerializer localDateTimeSerializer() {
return new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(pattern));
}
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer(
LocalDateTimeSerializer localDateTimeSerializer
) {
return builder -> builder.serializerByType(
LocalDateTime.class, localDateTimeSerializer
);
}
}
public interface ISysUserService extends IService<SysUser>, UserDetailsService {}
@Service
@Slf4j
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements ISysUserService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
//根据登录名称进行查询
SysUser user = this.lambdaQuery().eq(SysUser::getLogName, logName).one();
//根据用户ID查询对应的角色
List<SysRole> roleList = roleMapper.selectRoleByUserId(user.getId());
//组装权限对象
List<GrantedAuthority> authorities = new ArrayList<>();
roleList.forEach(role->{
authorities.add(new SimpleGrantedAuthority(role.getRoleName()))
});
//组装用户对象
return new User(
user.getLogName(),
passwordEncoder.encode(user.getLogPwd()),
authorities
);
}
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
//认证相关 用户名密码
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//基于数据库
auth.userDetailsService(userService).passwordEncoder(this.passwordEncoder());
}
Sys_user
实现UserDetails
接口,指定用户名,密码,角色集合以及账号状态SysRole
实现GrantedAuthority
接口,指定角色组装的名称- 在
SysRoleMapper
接口中添加一个方法selectRoleByUserId
用于角色集合查询- 在
SysUserMapper
接口中添加一个方法selectUserByLogName
用于登录查询
public interface SysRoleMapper extends BaseMapper<SysRole> {
@Select(" select id,role_name,remark,create_time from sys_user u " +
" join sys_user_role ur on u.id = ur.user_id " +
" join sys_role r on r.id = ur.role_id " +
" where u.id = #{id} ")
List<SysRole> selectRoleByUserId(Integer id);
}
public interface SysUserMapper extends BaseMapper<SysUser> {
@Results({
@Result(id = true,property = "id",column = "id"),
@Result(property = "roles",column = "id",javaType = List.class,
many = @Many(select = "org.neuedu.security.mapper.SysRoleMapper.selectRoleByUserId") )
})
@Select("select id,real_name,log_name,log_pwd,gender,disabled,remark,create_time from sys_user where log_name =#{logName} ")
SysUser selectUserByLogName(String logName);
}
@Service
@Slf4j
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements ISysUserService {
@Override
public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
return baseMapper.selectUserByLogName(name);
}
}
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private ISysUserService userService;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
//认证相关 用户名密码
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//基于数据库
auth.userDetailsService(userService).passwordEncoder(this.passwordEncoder());
}
//web资源,静态资源的配置
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/logPage"); //登录请求不加入 security 中
}
//授权,请求相关
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated();
//表单的配置
http.formLogin()
.usernameParameter("logName").passwordParameter("logPwd")
.loginPage("/logPage").loginProcessingUrl("/login");
//记住我功能
http.rememberMe().rememberMeParameter("forgetMe")
.rememberMeCookieName("forgetMe").tokenValiditySeconds(10);
//csrf
http.csrf().disable();
}
@RestController
@RequestMapping("/sysUser")
public class SysUserController {
@Autowired
private ISysUserService userService ;
@PreAuthorize("hasRole('USER')")
@GetMapping("/list")
public ResponseEntity list(){
return ResponseEntity.data(userService.list());
}
}
<mapper namespace="org.neuedu.security.mapper.SysAuthorityMapper">
<select id="selectByUserId" resultType="SysAuthority">
SELECT distinct a.* FROM sys_authority a
join sys_role_authority ra on ra.authority_id = a.id
join sys_role r on r.id = ra.role_id
join sys_user_role ur on ur.role_id = r.id
join sys_user u on u.id = ur.user_id
where u.id = #{userId}
<if test="type != null">
and type = #{type}
if>
order by a.id
select>
mapper>
@Service
@Slf4j
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements ISysUserService {
@Autowired
private SysAuthorityMapper authorityMapper;
@Override
public UserDetails loadUserByUsername(String logName) throws UsernameNotFoundException {
//1. 根据用户名去登录
SysUser currentUser = this.lambdaQuery().eq(SysUser::getLogName, logName).one();
if(Objects.isNull(currentUser)){
throw new UsernameNotFoundException("用户名或密码输入有误~");
}
//2. 查询权限 type = 1 , 0
List<SysAuthority> authorities = authorityMapper.selectByUserId(currentUser.getId(), null);
currentUser.setAuthorities(authorities);
return currentUser;
}
}
@PreAuthorize("hasAuthority('user:list')")
@GetMapping("/list")
@ResponseBody
public ResponseEntity list(){
return ResponseEntity.data(userService.list());
}
@PreAuthorize("hasAuthority('user:del')")
@GetMapping("/del")
public @ResponseBody ResponseEntity del(){
return ResponseEntity.ok("删除");
}
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private ISysUserService userService;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
//认证相关 用户名密码
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//基于数据库
auth.userDetailsService(userService).passwordEncoder(this.passwordEncoder());
}
//web资源,静态资源的配置
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/sysUser/logPage");
}
//授权,请求相关
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated();
//表单的配置
http.formLogin()
.usernameParameter("logName").passwordParameter("logPwd").loginProcessingUrl("/login")
.successHandler((httpServletRequest, httpServletResponse, authentication) -> {
ResponseEntity responseEntity = ResponseEntity.ok("认证成功!");
responseJSON(httpServletResponse, responseEntity);
})
.failureHandler((httpServletRequest, httpServletResponse, e) -> {
ResponseEntity responseEntity = null;
if(e instanceof UsernameNotFoundException || e instanceof BadCredentialsException){
responseEntity = ResponseEntity.exception(ResponseCode.USERNAME_PASSWORD_EXCEPTION);
}else if(e instanceof DisabledException){
responseEntity = ResponseEntity.exception(ResponseCode.ACCOUNT_DISABLED);
}else{
responseEntity = ResponseEntity.exception(ResponseCode.SYSTEM_EXCEPTION);
}
responseJSON(httpServletResponse,responseEntity);
})
.and()
.exceptionHandling()
.authenticationEntryPoint((httpServletRequest, httpServletResponse, e) -> {
ResponseEntity responseEntity = ResponseEntity.exception(ResponseCode.NEED_LOGIN);
responseJSON(httpServletResponse,responseEntity);
})
.accessDeniedHandler((httpServletRequest, httpServletResponse, e) -> {
ResponseEntity responseEntity = ResponseEntity.exception(ResponseCode.AUTHORIZE_EXCEPTION);
responseJSON(httpServletResponse,responseEntity);
});
//注销
http.logout()
.logoutUrl("/logout").invalidateHttpSession(true)
.logoutSuccessHandler((httpServletRequest, httpServletResponse, authentication) -> {
ResponseEntity responseEntity = ResponseEntity.ok("注销成功~");
responseJSON(httpServletResponse,responseEntity);
});
//记住我功能
http.rememberMe().rememberMeParameter("forgetMe").rememberMeCookieName("forgetMe").tokenValiditySeconds(10);
//csrf
http.csrf().disable();
}
private void responseJSON(HttpServletResponse httpServletResponse, ResponseEntity responseEntity) throws IOException {
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter out = httpServletResponse.getWriter();
try {
out.write(JsonUtil.ToStringIgnoreNull(responseEntity));
} catch (Exception e) {
System.out.println("JSON序列化错误");
}
out.close();
}
}
@Component
public class SecurityAuthorizeHandler implements AuthenticationSuccessHandler, AuthenticationFailureHandler, AccessDeniedHandler, AuthenticationEntryPoint, LogoutSuccessHandler {
private ResponseEntity responseEntity;
//认证成功
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
responseEntity = ResponseEntity.ok("认证成功!");
responseJSON(httpServletResponse, responseEntity);
}
//认证失败
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
if(e instanceof UsernameNotFoundException || e instanceof BadCredentialsException){
responseEntity = ResponseEntity.exception(ResponseCode.USERNAME_PASSWORD_EXCEPTION);
}else if(e instanceof DisabledException){
responseEntity = ResponseEntity.exception(ResponseCode.ACCOUNT_DISABLED);
}else{
responseEntity = ResponseEntity.exception(ResponseCode.SYSTEM_EXCEPTION);
}
responseJSON(httpServletResponse,responseEntity);
}
//403权限不足
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
responseEntity = ResponseEntity.exception(ResponseCode.AUTHORIZE_EXCEPTION);
responseJSON(httpServletResponse,responseEntity);
}
//非法访问
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
responseEntity = ResponseEntity.exception(ResponseCode.NEED_LOGIN);
responseJSON(httpServletResponse,responseEntity);
}
//注销成功
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
responseEntity = ResponseEntity.ok("注销成功~");
responseJSON(httpServletResponse,responseEntity);
}
//Response JSON 响应
private void responseJSON(HttpServletResponse httpServletResponse, ResponseEntity responseEntity) throws IOException {
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter out = httpServletResponse.getWriter();
try {
out.write(JsonUtil.ToStringIgnoreNull(responseEntity));
} catch (Exception e) {
System.out.println("JSON序列化错误");
}
out.close();
}
}
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private ISysUserService userService;
@Autowired
private SecurityAuthorizeHandler authorizeHandler;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
//认证相关 用户名密码
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//基于数据库
auth.userDetailsService(userService).passwordEncoder(this.passwordEncoder());
}
//web资源,静态资源的配置
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/sysUser/logPage");
}
//授权,请求相关
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated();
//表单的配置
http.formLogin()
.usernameParameter("logName").passwordParameter("logPwd").loginProcessingUrl("/login")
.successHandler(authorizeHandler)
.failureHandler(authorizeHandler)
.and()
.exceptionHandling()
.authenticationEntryPoint(authorizeHandler)
.accessDeniedHandler(authorizeHandler);
//注销
http.logout()
.logoutUrl("/logout").invalidateHttpSession(true)
.logoutSuccessHandler(authorizeHandler);
//记住我功能
http.rememberMe().rememberMeParameter("forgetMe").rememberMeCookieName("forgetMe").tokenValiditySeconds(10);
//csrf
http.csrf().disable();
}
}
ajax请求:
function login(){
$.ajax({
method:'post',
url:'/login',
dataType:'text', //需要注意返回数据类型,否则可能JSON解析失败,会进入error
data:{logName:'user',logPwd:'user'},
success:function (res) {
console.log('succ');
console.log(res);
},
error:function (res) {
console.log('error');
console.log(res);
}
});
}