1、人们使用Spring Secruity的原因有很多,单大部分都发现了javaEE的Servlet规范或EJB规范中的安全功能缺乏典型企业应用场景所需的深度。提到这些规范,重要的是要认识到他们在WAR或EAR级别无法移植。因此如果你更换服务器环境,这里有典型的大量工作去重新配置你的应用程序员安全到新的目标环境。使用Spring Security 解决了这些问题,也为你提供许多其他有用的,可定制的安全功能。
正如你可能知道的两个应用程序的两个主要区域是“认证”和“授权”(或者访问控制)。这两个主要区域是Spring Security 的两个目标。“认证”,是建立一个他声明的主题的过程(一个“主体”一般是指用户,设备或一些可以在你的应用程序中执行动作的其他系统)。“授权”指确定一个主体是否允许在你的应用程序执行一个动作的过程。为了抵达需要授权的目的,主体的身份已经有认证过程建立。这个概念是通用的而不只在Spring Security中。
Spring Security主要的组件图:
在身份验证层,Spring Security 的支持多种认证模式。这些验证绝大多数都是要么由第三方提供,或由相关的标准组织,如互联网工程任务组开发。另外Spring Security 提供自己的一组认证功能。具体而言,Spring Security 目前支持所有这些技术集成的身份验证:
HTTP BASIC 认证头 (基于 IETF RFC-based 标准)
HTTP Digest 认证头 ( IETF RFC-based 标准)
HTTP X.509 客户端证书交换 ( IETF RFC-based 标准)
LDAP (一个非常常见的方法来跨平台认证需要, 尤其是在大型环境)
Form-based authentication (用于简单的用户界面)
OpenID 认证
Authentication based on pre-established request headers (such as Computer Associates Siteminder) 根据预先建立的请求有进行验证
JA-SIG Central Authentication Service (CAS,一个开源的SSO系统 )
Transparent authentication context propagation for Remote Method Invocation (RMI) and HttpInvoker (Spring远程协议)
Automatic “remember-me” authentication (你可以勾选一个框以避免预定的时间段再认证)
Anonymous authentication (让每一个未经验证的访问自动假设为一个特定的安全标识)
Run-as authentication (在一个访问应该使用不同的安全标识时非常有用)
Java Authentication and Authorization Service (JAAS)
JEE container autentication (所以如果愿你以可以任然使用容器管理的认证)
Kerberos
Java Open Source Single Sign On (JOSSO) *
OpenNMS Network Management Platform *
AppFuse *
AndroMDA *
Mule ESB *
Direct Web Request (DWR) *
Grails *
Tapestry *
JTrac *
Jasypt *
Roller *
Elastic Path *
Atlassian Crowd *
Your own authentication systems (see below)
2、Spring Security常用的11个权限拦截器
SecurityContextPersistenceFilter:
这个过滤器位于顶端,是第一个起作用的过滤器
验证用户session是否存在,存在则放到SecurityContextHolder中,不存在则创建后到SecurityContextHolder中
另一个作用是在过滤器执行完毕后清空SecurityContextHolder中的内容
LogoutFilter:
在用户发出注销请求时,清除用户的session以及SecurityContextHolder中的内容
AbstractAuthenticationProcessingFilter:
处理from表单登录的过滤器
DefaultLoginPageGeneratingFilter:
用来生成一个默认的登录页面
BasicAuthenticationFilter:
用于Basic验证
SecurityContextHolderAwareRequestFilter:
用于包装用户的请求
目的是为后续的程序提供一些额外的数据
RememberMeAuthenticationFilter:
当用户cookie中存在RememberMe标记时,会根据标记自动实现用户登录,并创建SecurityContext,授予相应的权限
AnonymousAuthenticationFilter:
保证操作统一性,当用户没有登录时,默认为用户分配匿名用户的权限,可以选择关闭匿名用户
ExceptionTranslationFilter:
处理FilterSecurityInterceptor中所抛出的异常,然后将请求重定向到相应的页面,或响应错误信息。也就是作为一个处理全局异常的Filter
SessionManagementFilter:
用于防御会话伪造×××,会销毁用户的session,并重新生成一个session
FilterSecurityInterceptor:
用户的权限控制都包含在这里
如果用户未登陆就会抛出用户未登陆的异常
如果用户已登录但是没有访问当前资源的权限,就会抛出拒绝访问异常
如果用户已登录并具有访问当前资源的权限,则放行
以上就是Spring Security常用的11个权限拦截器,那么这些拦截器是按什么样的顺序执行的呢?这就需要先了解一下FilterChainProxy这个过滤器链代理类了:
FilterChainProxy可以按照指定的顺序调用一组Filter,使这组Filter既能完成验证授权的本职工作,又能享用Spring IOC的功能,来方便的得到其他依赖的资源
3、基于数据库的权限认证:
说明:因为springsecurity的构建代码都比较类似,这里就直接粘贴所需要配置的代码
第一步:
创建数据库,数据库表包含用户、角色、用户角色表、url表以及url与所需权限的表,如图:
sql语句:
/*
Navicat Premium Data Transfer
Source Server : mysql
Source Server Type : MySQL
Source Server Version : 80011
Source Host : localhost:3306
Source Schema : security
Target Server Type : MySQL
Target Server Version : 80011
File Encoding : 65001
Date: 07/09/2019 15:45:56
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for s_permission
-- ----------------------------
DROP TABLE IF EXISTS `s_permission`;
CREATE TABLE `s_permission` (
`id` int(11) NOT NULL,
`permission` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`url` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`describe` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of s_permission
-- ----------------------------
INSERT INTO `s_permission` VALUES (1, 'P_USER,P_ADMIN,P_HELLO', '/index', 'index页面资源');
INSERT INTO `s_permission` VALUES (2, 'P_ADMIN', '/admin', 'admin页面资源');
INSERT INTO `s_permission` VALUES (3, 'P_HELLO,P_ADMIN', '/helloV', 'hello页面资源');
INSERT INTO `s_permission` VALUES (4, 'P_USER,P_ADMIN,P_HELLO', '/helloU', 'hello USER的界面');
-- ----------------------------
-- Table structure for s_role
-- ----------------------------
DROP TABLE IF EXISTS `s_role`;
CREATE TABLE `s_role` (
`id` int(11) NOT NULL,
`role` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`describe` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of s_role
-- ----------------------------
INSERT INTO `s_role` VALUES (1, 'R_ADMIN', '大总管,所有权限');
INSERT INTO `s_role` VALUES (2, 'R_HELLO', '说hello相关的权限');
INSERT INTO `s_role` VALUES (3, 'R_USER', '访问用户界面以及首页');
-- ----------------------------
-- Table structure for s_role_permission
-- ----------------------------
DROP TABLE IF EXISTS `s_role_permission`;
CREATE TABLE `s_role_permission` (
`fk_role_id` int(11) NULL DEFAULT NULL,
`fk_permission_id` int(11) NULL DEFAULT NULL,
INDEX `union_key`(`fk_role_id`, `fk_permission_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of s_role_permission
-- ----------------------------
INSERT INTO `s_role_permission` VALUES (1, 1);
INSERT INTO `s_role_permission` VALUES (1, 2);
INSERT INTO `s_role_permission` VALUES (1, 3);
INSERT INTO `s_role_permission` VALUES (1, 4);
INSERT INTO `s_role_permission` VALUES (1, 5);
INSERT INTO `s_role_permission` VALUES (2, 1);
INSERT INTO `s_role_permission` VALUES (2, 3);
INSERT INTO `s_role_permission` VALUES (2, 4);
INSERT INTO `s_role_permission` VALUES (2, 5);
INSERT INTO `s_role_permission` VALUES (3, 1);
INSERT INTO `s_role_permission` VALUES (3, 4);
INSERT INTO `s_role_permission` VALUES (3, 5);
-- ----------------------------
-- Table structure for s_user
-- ----------------------------
DROP TABLE IF EXISTS `s_user`;
CREATE TABLE `s_user` (
`id` int(11) NOT NULL,
`name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`password` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of s_user
-- ----------------------------
INSERT INTO `s_user` VALUES (1, 'admin', 'admin');
INSERT INTO `s_user` VALUES (2, 'veiking', 'veiking');
INSERT INTO `s_user` VALUES (3, 'xiaoming', 'xiaoming');
-- ----------------------------
-- Table structure for s_user_role
-- ----------------------------
DROP TABLE IF EXISTS `s_user_role`;
CREATE TABLE `s_user_role` (
`fk_user_id` int(11) NULL DEFAULT NULL,
`fk_role_id` int(11) NULL DEFAULT NULL,
INDEX `union_key`(`fk_user_id`, `fk_role_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of s_user_role
-- ----------------------------
INSERT INTO `s_user_role` VALUES (1, 1);
INSERT INTO `s_user_role` VALUES (2, 2);
INSERT INTO `s_user_role` VALUES (2, 3);
INSERT INTO `s_user_role` VALUES (3, 3);
SET FOREIGN_KEY_CHECKS = 1;
第二步:
通过逆向工程生成对应的实体类以及对应的mapper
第三 步:
创建springboot工程,加入关于springSecurity的jar包,pom.xml的文件如下:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<!--添加对mybatis的依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!-- 数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.15</version>
</dependency>
<!--对druid的依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
<!--对thymeleaf的支持-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--加入对热部署的配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
</build>
第四步:
创建一个实体类实现UserDetails接口,用来当作用户登陆时的认证,代码如下:
package com.mybatis.my.bean;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* @author 17715
*/
public class UserDetailImp implements UserDetails {
private User user;
/**
* 一个用户对应多个角色
*/
private List<Role> roles;
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> list=new ArrayList<>();
for(Role role:roles){
//遍历角色
list.add(new SimpleGrantedAuthority(role.getRole()));
}
return list;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
第五步:
编写server层,继承UserDetailsService,重写其方法:
第六步:
编写路径拦截器类,需要实现FilterInvocationSecurityMetadataSource接口,Collection getAttributes(Object object)会返回改路径所需的角色集合到权限决策处理类中供其使用.
第七步:
权限决策处理,需要实现 AccessDecisionManager接口。实现方法decide(Authentication authentication, Object object, Collection configAttributes) 来做权限决策
authentication可获取当前用户拥有的角色集合
configAttributes 路径拦截处理中查询到的,能访问当前路径的角色集合
当url所需的要角色,用户不具有时,抛出AccessDeniedException异常,
第八步:
当抛出权限不足异常时,我们需要捕获,这时创建一个捕获AccessDeniedException的类,继承AccessDeniedHandler,代码如下:
package com.mybatis.my.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mybatis.my.bean.RespBean;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 用于捕获权限不足的异常,向页面写出权限不足
*/
@Component
public class AuthenticationAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse resp, AccessDeniedException e) throws IOException, ServletException {
resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
resp.setContentType("application/json;charset=UTF-8");
PrintWriter out = resp.getWriter();
RespBean error = RespBean.error("权限不足,请联系管理员!");
out.write(new ObjectMapper().writeValueAsString(error));
out.flush();
out.close();
}
}
第九步:
配置安全权限的类(重点),继承WebSecurityConfigurerAdapter,先粘贴代码,后再进行说明:
package com.mybatis.my.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mybatis.my.bean.RespBean;
import com.mybatis.my.common.UserUtils;
import com.mybatis.my.server.UserServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.*;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import java.io.PrintWriter;
/**
* @author 17715
*/
@Configuration
//增加权限
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserServer userServer;
@Autowired
CustomMetadataSource metadataSource;
@Autowired
UrlAccessDecisionManager urlAccessDecisionManager;
@Autowired
AuthenticationAccessDeniedHandler deniedHandler;
@Override
public void configure(WebSecurity web) throws Exception {
//不拦截静态资源的访问
web.ignoring().antMatchers("/js/**", "/css/**", "/images/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
/* http.authorizeRequests() //开启登陆验证
.antMatchers("/").permitAll()
.anyRequest().authenticated()
.and().logout().permitAll()
.and().formLogin();*/
http.authorizeRequests()
.antMatchers("/login", "/login_p").permitAll()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
o.setSecurityMetadataSource(metadataSource);
o.setAccessDecisionManager(urlAccessDecisionManager);
return o;
}
})
.and()
.formLogin()/*.loginPage("/login").loginProcessingUrl("/login_p")*/
.usernameParameter("username").passwordParameter("password")
.failureHandler((req, resp, e) -> {
resp.setContentType("application/json;charset=utf-8");
RespBean respBean = null;
if (e instanceof BadCredentialsException ||
e instanceof UsernameNotFoundException) {
respBean = RespBean.error("账户名或者密码输入错误!");
} else if (e instanceof LockedException) {
respBean = RespBean.error("账户被锁定,请联系管理员!");
} else if (e instanceof CredentialsExpiredException) {
respBean = RespBean.error("密码过期,请联系管理员!");
} else if (e instanceof AccountExpiredException) {
respBean = RespBean.error("账户过期,请联系管理员!");
} else if (e instanceof DisabledException) {
respBean = RespBean.error("账户被禁用,请联系管理员!");
} else {
respBean = RespBean.error("登录失败!");
}
resp.setStatus(401);
ObjectMapper om = new ObjectMapper();
PrintWriter out = resp.getWriter();
out.write(om.writeValueAsString(respBean));
out.flush();
out.close();
})
.successHandler((req, resp, auth) -> {
resp.setContentType("application/json;charset=utf-8");
RespBean respBean = RespBean.ok("登录成功!", UserUtils.getCurrentUser().getUser());
ObjectMapper om = new ObjectMapper();
PrintWriter out = resp.getWriter();
out.write(om.writeValueAsString(respBean));
out.flush();
out.close();
})
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler((req, resp, authentication) -> {
resp.setContentType("application/json;charset=utf-8");
RespBean respBean = RespBean.ok("注销成功!");
ObjectMapper om = new ObjectMapper();
PrintWriter out = resp.getWriter();
out.write(om.writeValueAsString(respBean));
out.flush();
out.close();
})
.permitAll()
.and().csrf().disable()
.exceptionHandling().accessDeniedHandler(deniedHandler);
// 禁用缓存
http.headers().cacheControl();
/* http.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class);*/
}
/**
* 用于登陆的验证
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userServer).passwordEncoder(new BCryptPasswordEncoder());
}
}
这里对几个重要的地方截图说明:
不拦截静态资源文件的方法:
设置权限等方法
基于数据库的权限管理用户配置:
第十步:
创建contoller,对请求的url进行转发
这样子就大功告成了,启动程序,运行之后的截图:
附:这里给出源码地址,需要的小伙伴可以参考:
源码地址