文章目录
Spring Security基于servlet过滤器实现的权限管理框架,是AOP思想的具体体现,提供了完善的认证机制和方法级的授权功能。本文基于springmvc项目。
1 入门案例
查询数据库用户信息,实现登录认证。
1.1 引入依赖
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.3.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>5.3.1.RELEASE</version>
</dependency>
1.2 web配置
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<!--spring容器,是父容器,spring-mvc和spring-security为子容器,导入到父容器中-->
<param-value>classpath:applicationContext.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!--编码过滤器,解决乱码问题-->
<filter>
<filter-name>characterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!--过滤器链,对象名称不能改成其他-->
<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>
</web-app>
1.3 security配置
创建spring-security.xml文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd">
<!--设置不经过SpringSecurity过滤器的静态资源-->
<security:http pattern="/css/**" security="none"/>
<security:http pattern="/img/**" security="none"/>
<security:http pattern="/plugins/**" security="none"/>
<security:http pattern="/errors/**" security="none"/>
<!--设置可以用el表达式配置,并自动生成对应过滤器-->
<security:http auto-config="true" use-expressions="true">
<!--指定可以被匿名访问的页面-->
<security:intercept-url pattern="/login.jsp" access="permitAll()"/>
<!--使用el表达式来指定项目所有资源访问都必须有ROLE_USER或ROLE_ADMIN角色,才能访问-->
<security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER','ROLE_ADMIN')"/>
<!--指定自定义的登录页面-->
<security:form-login login-page="/login.jsp"
login-processing-url="/login" default-target-url="/index.jsp"
authentication-failure-url="/errors/failer.jsp"/>
<!--指定退出后跳转到哪个页面-->
<security:logout logout-url="/logout"
logout-success-url="/login.jsp"/>
<!--登录页面中记住我功能,开启remember-me过滤器,设置token存储时间为60秒,此处存储在cookie中,也可设置存储在数据库中-->
<security:remember-me token-validity-seconds="60"/>
</security:http>
<!--设置认证用户信息的来源-->
<security:authentication-manager>
<!--从数据库中查找用户名和密码,user-service-ref指定查询服务对象-->
<security:authentication-provider user-service-ref="userService">
<!--下面设置固定用户名和密码-->
<!--<security:user-service>-->
<!--{noop}指定password不加密-->
<!--<security:user name="user" password="{noop}user" authorities="ROLE_USER"/>-->
<!--</security:user-service>-->
<!--指定加密对象-->
<security:password-encoder ref="passwordEncoder"/>
</security:authentication-provider>
</security:authentication-manager>
</beans>
主配置文件applicationContext.xml中引入spring-security.xml,创建加密对象(用于密码加密保存和加密认证):
<import resource="classpath:spring-security.xml"/>
<!--加密对象-->
<bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
1.4 查询用户
userService实现:
@Service("userService")
public class UserServiceImp implements UserService {
@Resource
private UserDao userDao;
@Resource
private BCryptPasswordEncoder passwordEncoder;
public List<User> selectAll() {
return userDao.findAll();
}
public void addUser(User user) {
user.setPassword(passwordEncoder.encode(user.getPassword()));//加密存储
userDao.addUser(user);
}
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
List<User> users = userDao.findByUserName(username);
if(users == null || users.size() == 0){
throw new UsernameNotFoundException(String.format("JdbcDaoImpl.notFound %s",username));
}
User user = users.get(0);
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
for(Role role : user.getRoles()){
grantedAuthorities.add(new SimpleGrantedAuthority(role.getRoleName()));
}
//密码前拼接"{noop}",表示密码不加密验证,不加,需要加密验证
//本地项目中的User类和spring security中的冲突了,直接带包名创建
return new org.springframework.security.core.userdetails.User(user.getUserName(),user.getPassword(),user.getStatus()==1,true,true,true,grantedAuthorities) ;
}
public void updateUser(User user) {
user.setPassword(passwordEncoder.encode(user.getPassword()));
userDao.updateUser(user);
}
}
要事先准备好用户表、角色表、权限表、用户角色表、角色权限表,并添加一些数据。
--先删除关联表
DROP TABLE IF EXISTS `SYS_ROLE_PERMISSION`;
DROP TABLE IF EXISTS `SYS_USER_ROLE`;
DROP TABLE IF EXISTS `SYS_USER`;
CREATE TABLE `SYS_USER` (
`ID` INT(11) NOT NULL AUTO_INCREMENT,
`USER_NAME` VARCHAR(32) 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`)
) ENGINE=INNODB DEFAULT CHARSET=UTF8;
DROP TABLE IF EXISTS `SYS_ROLE`;
CREATE TABLE `SYS_ROLE` (
`ID` INT(11) NOT NULL AUTO_INCREMENT COMMENT '编号',
`ROLE_NAME` VARCHAR(30) DEFAULT NULL COMMENT '角色名称',
`ROLE_DESC` VARCHAR(60) DEFAULT NULL COMMENT '角色描述',
PRIMARY KEY (`ID`)
) ENGINE=INNODB DEFAULT CHARSET=UTF8;
CREATE TABLE `SYS_USER_ROLE` (
`UID` INT(11) NOT NULL COMMENT '用户编号',
`RID` INT(11) NOT NULL COMMENT '角色编号',
PRIMARY KEY (`UID`,`RID`),
CONSTRAINT `FK_REFERENCE_1` FOREIGN KEY (`RID`) REFERENCES `SYS_ROLE` (`ID`),
CONSTRAINT `FK_REFERENCE_2` FOREIGN KEY (`UID`) REFERENCES `SYS_USER` (`ID`)
) ENGINE=INNODB DEFAULT CHARSET=UTF8;
DROP TABLE IF EXISTS `SYS_PERMISSION`;
CREATE TABLE `SYS_PERMISSION` (
`ID` INT(11) NOT NULL AUTO_INCREMENT COMMENT '编号',
`PERMISSION_NAME` VARCHAR(30) DEFAULT NULL COMMENT '菜单名称',
`PERMISSION_URL` VARCHAR(100) DEFAULT NULL COMMENT '菜单地址',
`PARENT_ID` INT(11) NOT NULL DEFAULT '0' COMMENT '父菜单ID',
PRIMARY KEY (`ID`)
) ENGINE=INNODB DEFAULT CHARSET=UTF8;
CREATE TABLE `SYS_ROLE_PERMISSION` (
`RID` INT(11) NOT NULL COMMENT '角色编号',
`PID` INT(11) NOT NULL COMMENT '权限编号',
PRIMARY KEY (`RID`,`PID`),
CONSTRAINT `FK_REFERENCE_3` FOREIGN KEY (`RID`) REFERENCES `SYS_ROLE` (`ID`),
CONSTRAINT `FK_REFERENCE_4` FOREIGN KEY (`PID`) REFERENCES `SYS_PERMISSION` (`ID`)
) ENGINE=INNODB DEFAULT CHARSET=UTF8;
1.5 登录页面
login.jsp页面中添加标签库,表单标签下添加标签<security:csrfInput/>
<%@taglib prefix="security" uri="http://www.springframework.org/security/tags"%>
<form method="post">
<security:csrfInput/>
<!--记住我,name属性值必须为remember-me-->
<input type="checkbox" name="remember-me" value="true">
</form>
还需要写个login之后的跳转Controller,最后启动tomcat,进行登录验证。
“记住我”功能补充说明:
将用户名密码和token都存在客户端相对不安全,另一种方法是在cookie中仅保存一个无意义的加密串,在数据库中保存加密串与登录用户(用户名、密码、token、时间)的对应关系,下次自动登录时,cookie中的加密串与数据库中的数据一同进行验证。
添加存储表PERSISTENT_LOGINS:
CREATE TABLE `PERSISTENT_LOGINS` (
`SERIES` VARCHAR(64) NOT NULL,
`USERNAME` VARCHAR(64) NOT NULL,
`TOKEN` VARCHAR(64) NOT NULL,
`LAST_USED` TIMESTAMP NOT NULL,
PRIMARY KEY (`SERIES`)
) ENGINE=INNODB DEFAULT CHARSET=UTF8;
spring-security.xml中:
<security:remember-me data-source-ref="dataSource" token-validity-seconds="60" />
2 过滤器
spring security的核心是过滤器,一系列过滤器组成了过滤器链,如下图:
序号 | 过滤器 | 说明 |
---|---|---|
1 | SecurityContextPersistenceFilter | 使用SecurityContextRepository在session中保存或更新一个 SecurityContext,并将SecurityContext给过滤器链中之后的过滤器使用。 SecurityContext中存储了当前用户的认证以及权限信息。 |
2 | WebAsyncManagerIntegrationFilter | 用于集成SecurityContext到Spring异步执行机制中的WebAsyncManager。 |
3 | HeaderWriterFilter | 向请求的Header中添加信息,可在<security:http>标签内部使用<security:headers>来控制 |
4 | CsrfFilter | 跨域请求伪造过滤,对所有post请求验证是否包含系统生成的csrf的token信息, 如果不包含,则报错。可防止csrf攻击。<security:csrf>可设置禁用,如果为开启,则必须为post请求。 |
5 | LogoutFilter | 匹配URL为/logout的请求,实现用户退出,清除认证信息。 |
6 | UsernamePasswordAuthenticationFilter | 主要负责认证的过滤器,默认匹配URL为/login且必须为POST请求。 |
7 | DefaultLoginPageGeneratingFilter | 如果没有在配置文件中指定认证页面,则由该过滤器生成一个默认认证页面。 |
8 | DefaultLogoutPageGeneratingFilter | 生成一个默认的退出登录页面 |
9 | BasicAuthenticationFilter | 自动解析HTTP请求中头部名字为Authentication,且以Basic开头的头信息。 |
10 | RequestCacheAwareFilter | 通过HttpSessionRequestCache内部维护了一个RequestCache,用于缓存HttpServletRequest |
11 | SecurityContextHolderAwareRequestFilter | 针对ServletRequest进行了一次包装,使得request具有更加丰富的API |
12 | AnonymousAuthenticationFilter | 当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder中。为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名的身份。 |
13 | SessionManagementFilter | SecurityContextRepository限制同一用户开启多个会话的数量 |
14 | ExceptionTranslationFilter | 异常转换过滤器,用来转换整个链中出现的异常 |
15 | FilterSecurityInterceptor | 获取所配置资源访问的授权信息,根据SecurityContextHolder中存储的用户信息来判断其是否有权限。 |
3 页面动态展示
标签 | 说明 |
---|---|
<security:authorize access=“hasAnyRole(‘ROLE_ADMIN’)”> | 页面中某些内容需要根据角色显示时,用<security:authorize>包裹 |
<security:authentication property=“principal.username”/> | 显示用户名,property属性值也可以为name |
4 权限控制
通过注解的方式来控制类或者方法的访问权限。
4.1 开启注解支持
application.xml或者spring-mvc.xml中添加配置:
<!--开启security,jsr250,spring注解支持-->
<security:global-method-security secured-annotations="enabled" jsr250-annotations="enabled" pre-post-annotations="enabled"/>
4.2 添加注解
Controller或者Service类或者方法上添加注解:
注解 | 说明 |
---|---|
@Secured({“ROLE_ADMIN”,“ROLE_USER”}) | security的注解,有ROLE_ADMIN或ROLE_USER角色才能访问 |
@RolesAllowed({“ROLE_ADMIN”,“ROLE_USER”}) | jsr250的注解 |
@PreAuthorize(“hasAnyRole(‘ROLE_ADMIN’,‘ROLE_USER’)”) | spring的注解 |
4.3 友好页面展示
禁止访问时,友好页面展示的方式:
4.3.1 security配置文件设置
spring-security.xml中<security:http>下添加:
<security:access-denied-handler error-page="/errors/403.jsp"/>
4.3.2 web配置文件设置
web.xml中添加:
<error-page>
<error-code>403</error-code>
<location>/errors/403.jsp</location>
</error-page>
4.3.3 异常处理器
@ControllerAdvice
public class ControllerException {
@ExceptionHandler(AccessDeniedException.class)
public String exception403Advice(){
return"redirect:/errors/403.jsp";
}
}