初识权限管理
权限管理,一般指根据系统设置的安全规则或者安全策略,用户可以访问而且只能访问自己被授权的资源。权限管理几乎出现在任何系统里面,前提是需要有用户和密码认证的系统。
在权限管理的概念中,有两个非常重要的名词:
认证
:通过用户名和密码成功登陆系统后,让系统得到当前用户的角色身份。
授权
:系统根据当前用户的角色,给其授予对应可以操作的权限资源。
完成权限管理需要三个对象:
用户
:主要包含用户名,密码和当前用户的角色信息,可实现认证操作。角色
:主要包含角色名称,角色描述和当前角色拥有的权限信息,可实现授权操作。权限
:权限也可以称为菜单,主要包含当前权限名称,url地址等信息,可实现动态展示菜单。
这也是经典的 RBAC 模式:
RBAC 是基于角色的访问控制(
Role-Based Access Control
)在 RBAC 中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予给角色,而把角色又赋予用户,这样的权限设计很清楚,管理起来很方便。
初识Spring Security
Spring Security
是 spring
采用AOP思想,基于 servlet
过滤器实现的安全框架。它提供了完善的认证机制
和方法级的授权
功能。是一款非常优秀的权限管理框架。
入门案例
添加 SpringSecurity 坐标
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
核心文件配置 - mvc 资源启用
<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 />
核心文件配置 - 认证和资源拦截
<!--用户认证: 过滤器链转发到当前配置,进行 资源拦截,页面请求处理....-->
<!--设置可以用spring的el表达式配置Spring Security并自动生成对应配置组件(过滤器)-->
<!-- auto-config="true" use-expressions="true" -->
<security:http >
<!--使用spring的el表达式来指定项目所有资源访问都必须进行认证-->
<security:intercept-url pattern="/**" access="hasRole('ROLE_ADMIN')"/>
<security:form-login />
<!-- <security:http-basic /> -->
</security:http>
<!--用户授权: 设置Spring Security认证用户信息的来源-->
<security:authentication-manager>
<security:authentication-provider>
<!--构建用户对象-->
<security:user-service>
<!--{noop} -- noop是no operate的意思,也就是说明保存的密码没有做加密操作-->
<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"
自动配置,加上它,则可以省略 认证方式,即<security:form-login />
可以不用
use-expressions="true"
启用 SPEL 表达式,页面可以获取响应的认证对象
<security:form-login />
页面表单的形式认证
<security:http-basic />
页面弹出框的形式认证
配置 SpringSecurity 过滤器
<!--Spring Security过滤器链,注意过滤器名称必须叫springSecurityFilterChain-->
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- SpringMVC 中央处理器 -->
<servlet>
<servlet-name>springMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:application-*.xml</param-value>
</init-param>
<!--服务器启动就加载Servlet-->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springMVC</servlet-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
提供的这个默认登录页面,不然不仅项目色调不一致,语言类型也不一致。
SpringSecurity使用自定义认证页面
查看 SringSecurity
默认提供的登录界面,获取对应的默认数据: 请求方式
,字段名称
, 请求地址
…
在 SpringSecurity
主配置文件中指定认证页面配置信息,注意:登录页面需要放行,否则会出现死循环
<!--用户认证-->
<!--直接释放无需经过SpringSecurity过滤器的静态资源-->
<security:http pattern="/fail.jsp" security="none" />
<security:http auto-config="true" use-expressions="true">
<!--指定login.jsp页面可以被匿名访问,注意 pattern 必须以 / 开头 -->
<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" />
<!--禁用crsf防护-->
<security:csrf disabled="true" />
<!--权限不足跳转的403页面-->
<security:access-denied-handler error-page="/fail.jsp" />
</security:http>
<!--用户授权-->
<!--设置Spring Security认证用户信息的来源-->
<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>
SpringSecurity 中的 SpringEL表达式
表达式 = 描述
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吗?问题就在这了!
SpringSecurity的csrf防护机制
CSRF(Cross-site request forgery)跨站请求伪造,是一种难以防范的网络攻击方式。
SpringSecurity 的 csrf 机制把请求方式分成两类来处理 - 【 CsrfFilter 】。
第一类
:“GET
”, “HEAD
”, “TRACE
”, "OPTIONS
"四类请求可以直接通过.
第二类
:除去上面四类,包括POST
都要被验证携带token
或者 关闭csrf
防护才能通过.
- 在
SpringSecurity
主配置文件中添加禁用crsf防护的配置 :<security:csrf disabled="true"/>
- 在认证页面携带
token
请求: 导入SpringSecurity
标签库,在表单中录入:<security:csrfInput />
注意:一旦开启了csrf防护功能,logout处理器便只支持POST请求方式了!
SpringSecurity 原理分析
SpringSecurity 流程图
- 客户端发起一个请求,进入 Security 过滤器链
- 当到 LogoutFilter 的时候判断是否是登出路径,如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理,如果登出失败则由 ExceptionTranslationFilter ;如果不是登出路径则直接进入下一个过滤器。
- 当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作,如果登录失败则到 AuthenticationFailureHandler 登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler 登录成功处理器处理,如果不是登录请求则不进入该过滤器。
- 当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到 Controller 层否则到 AccessDeniedHandler 鉴权失败处理器处理。
Spring Security常用过滤器介绍
- SecurityContextPersistenceFilter : 在每次请求处理之前将该请求相关的安全上下文信息加载到
SecurityContextHolder
中,然后在该次请求处理完成之后,将SecurityContextHolder
中关于这次请求的信息存储到一个“仓储”中,然后将SecurityContextHolder
中的信息清除,例如在Session中维护一个用户的安全信息就是这个过滤器处理的。 - CsrfFilter : 用于处理跨站请求伪造
- UsernamePasswordAuthenticationFilter : 用于处理基于表单的登录请求,从表单中获取用户名和密码。认证操作全靠这个过滤器,默认匹配URL为
/login
且必须为POST
请求, 默认使用的表单 name 值为username
和password
,这两个值可以通过设置这个过滤器的usernameParameter
和passwordParameter
两个参数的值进行修改。 - DefaultLoginPageGeneratingFilter : 如果没有在配置文件中指定认证页面,则由该过滤器生成一个默认认证页面。
- LogoutFilter : 匹配URL为
/logout
的请求,实现用户退出,清除认证信息。
- DefaultLogoutPageGeneratingFilter : 由此过滤器可以生产一个默认的退出登录页面
- AnonymousAuthenticationFilter : 当
SecurityContextHolder
中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder
中。 - FilterSecurityInterceptor : 以看做过滤器链的出口。获取所配置资源访问的授权信息,根据
SecurityContextHolder
中存储的用户信息来决定其是否有权限。 - RememberMeAuthenticationFilter : 当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的
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>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>6.0.6</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.16</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.6</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.14</version>
</dependency>
<!--mp 代码生成器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.28</version>
</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
的子类是不是就可以了呢?
使用Java多态性,简化代码
数据库实体关系
@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;
}
}
指定认证使用的业务对象
<!--设置Spring Security认证用户信息的来源-->
<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"/>
<!--设置Spring Security认证用户信息的来源-->
<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()
, 否则记住我这个功能无法成功。
<!--设置可以用spring的el表达式配置Spring Security并自动生成对应配置组件(过滤器)-->
<security:http auto-config="true" use-expressions="true">
<!--
开启remember me过滤器,
data-source-ref="dataSource" 指定数据库连接池 需要手动创建一个存储cookie的数据表
token-validity-seconds="60" 设置token存储时间为60秒 可省略
remember-me-parameter="remember-me" 指定记住的参数名 可省略
-->
<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" /> <!--项目启动前,创建 cookie 存储数据表,但是下次再启动项目的时候需要删除该字段--> <property name="createTableOnStartup" value="true" /> </bean>
用户资源动态授权
控制每一个前端请求的权限,配置如下:
<!--放行-->
<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时表示拥有对应的权限
需要注意的是因为access属性是使用表达式的,需要设置http元素的use-expressions="true"存储。ifAllGranted
: 不能使用表达式,由逗号分隔的权限列表,用户必须拥有所有列出的权限时显示ifAnyGranted
: 不能使用表达式,用户必须至少拥有其中的一个权限时才能显示ifNotGranted
: 不能使用表达式,用户未拥有所有列出的权限时才能显示
authentication
: 用来代表当前Authentication
对象,主要用于获取当前Authentication
的相关信息property
: 主要属性,我们可以通过它来获取当前Authentication
对象的相关信息scope
: 定义var存在的范围,默认是pageContext
var
: 定义一个变量 , 将其以指定的属性名进行存放,默认是存放在pageConext
中htmlScape
: 是否需要将html
进行转义。默认为true
。
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>5.1.5.RELEASE</version>
</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">admin</a>
</sec:authorize>
<!-- 只需拥有其中任意一个权限 -->
<sec:authorize ifAnyGranted="ROLE_USER,ROLE_ADMIN">hello</sec:authorize>
<!-- 不允许拥有指定的任意权限 -->
<sec:authorize ifNotGranted="ROLE_ADMIN">
<a href="user.jsp">user</a>
</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
.
JSR-250 注解使用
-
@RolesAllowed({"USER","ADMIN"})
具有两种权限中的一种,就可以访问。这里可以省略前缀ROLE_
-
@PermitAll
表示允许所有的角色进行访问,也就是说不进行权限控制 -
@DenyAll
表示无论什么角色都不可以访问,与@PermitAll
相反
@Secured注解
与 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";
}
}
整合 SpringBoot
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
@RestController
@RequestMapping("/product")
public class ProductController {
@RequestMapping
public String hello(){
return "success";
}
}
SpringBoot
已经为SpringSecurity
提供了默认配置,默认所有资源都必须认证通过才能访问,那么问题来了!此刻并没有连接数据库,也并未在内存中指定认证用户,如何认证呢?其实SpringBoot已经提供了默认用户名 user ,密码在项目启动时随机生成,在日志中可以查看到:
整合 Jsp 模板
SpringBoot 官方是不推荐在 SpringBoot 中使用 jsp 的,那么到底可以使用吗?答案是肯定的!
不过需要导入 tomcat 插件启动项目,不能再用 SpringBoot 默认 tomcat 了。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
核心配置文件中配置视图解析器
spring.mvc.view.prefix=/pages/
spring.mvc.view.suffix=.jsp
提供 SpringSecurity 配置类
@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>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.2.0</version>
</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";
}
}
整合Thymeleaf
认证
使用默认用户名 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);
}
});
}