SpringSecurity
一、概述
Spring Security的前身是Acegi Security ,是Spring项目中用来提供安全认证服务的框架,为基于J2EE企业应用软件提供了全面安全服务。
Spring Security 底层就是实现了一个 过滤器 接口
1). 关键名词
- 认证:是为用户建立一个他所声明的主体。主题一般式指用户,设备或可以在你系 统中执行动作的其他系统
- 授权:指的是一个用户能否在你的应用中执行某个操作,在到达授权判断之前,身份的主题已经由 身份验证过程建立了。
二、基本配置,用户登陆认证
1). maven坐标依赖
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.0.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.0.1.RELEASE</version>
</dependency>
2). 配置web.xml
...
<!-- 配置监听器,监听servletContext域的创建和销毁 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- 配置加载类路径的配置文件;
建议将 springSecurity.xml 配置文件的加载放入applicationContext.xml中!
-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:applicationContext.xml,classpath*:springSecurity.xml</param-value>
</context-param>
<!-- 委派过滤器, SpringSecurity 框架提供的过滤器
名字必须为 springSecurityFilterChain !底层决定的!
在容器中SpringSecurity框架自己创建的这个过滤器bean的id就是springSecurityFilterChain !
真实的实现类类型为FilterChainProxy
-->
<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>
...
3). 配置springSecurity.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:security="http://www.springframework.org/schema/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd">
<!-- 配置不过滤的资源(静态资源及登录相关) -->
<security:http security="none" pattern="/login.html" />
<security:http security="none" pattern="/css/**" />
<security:http security="none" pattern="/img/**" />
<security:http security="none" pattern="/pages/**" />
<security:http security="none" pattern="/plugins/**" />
<!-- auto-config="true" 默认使用框架提供的简洁的登陆页面
use-expressions="false" 是否使用SPEL表达式,(忽略)
-->
<security:http auto-config="true" use-expressions="false">
<!--
pattern 拦截的规则,支持部分正则,
access 访问系统的用户所需要的角色列表,满足一个即可 -->
<security:intercept-url pattern="/**" access="ROLE_USER,ROLE_ADMIN" />
<!-- 自定义登陆页面,
login-page 登陆页面
login-processing-url : 发送登陆请求数据的url
authentication-failure-url 用户权限校验失败后后才会跳转到这个页面,
如果数据库中没有这个用户则不会跳转到这个页面。
default-target-url 登陆成功后跳转的页面。
always-use-default-target: true 适用于后台管理系统,防止访问历史记录
false 适用于前台页面,提升用户体验
注:登陆页面参数建议固定:用户名:username; 密码:password; action:login -->
<security:form-login login-page="/login.html"
login-processing-url="/login"
username-parameter="username"
password-parameter="password"
authentication-failure-url="/login.html"
default-target-url="/index.html"
authentication-success-forward-url="/index.html"
always-use-default-target="true"/>
<!-- 登出,默认底层会自动使用 session技术保存用户状态!
invalidate-session 是否删除session
logout-url:登出处理链接,发送这个请求,当前用户就退出了登陆
logout-success-url:登出成功页面 -->
<security:logout invalidate-session="true" logout-url="/logout"
logout-success-url="/login.html" />
<!-- 关闭CSRF 跨越请求,默认是开启的 -->
<security:csrf disabled="true" />
<!-- 让框架允许使用内置框架页面 -->
<security:headers>
<security:frame-options policy="SAMEORIGIN"/>
</security:headers>
</security:http>
<!-- 配置认证信息来源,从数据库中获取;
默认用户名名称为 username;密码默认名称为 password ;
如果密码是没有经过加密处理的,需要在
密码字符前面加上 {noop} 前缀让框架识别为非加密的密码!
user-service-ref 配置的为 ioc 容器中 service层对应查询封装
用户信息的的id!需要开发者实现! -->
<security:authentication-manager>
<security:authentication-provider user-service-ref="userService">
<!-- 配置加密的方式: 使用下方的bean passwordEncoder-->
<security:password-encoder ref="passwordEncoder"/>
</security:authentication-provider>
</security:authentication-manager>
<!-- 配置加密类 -->
<bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
<!-- 配置登陆认证信息,下面的配置为文件形式的 用户名 和密码, 了解即可
<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>
-->
</beans>
4). UserService接口的继承接口实现类
import org.springframework.security.core.userdetails.UserDetailsService;
public interface UserService extends UserDetailsService {}
@Service("userService")
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
public UserDetails loadUserByUsername(String username) {
// userInfo 类包含了用户信息,以及用户关联的角色信息
UserInfo userInfo = userDao.findByUsername(username);
// 获取角色信息
List<Role> roles = userInfo.getRoles();
// 将角色信息封装到框架指定的类型容器中
List<SimpleGrantedAuthority> authorities = getAuthority(roles);
/** 【注意】:下方使用的User类是框架提供的!将上面查询的用户信息,角色信息封装到框架要求的容器中!
org.springframework.security.core.userdetails.User
是UserDetails的实现类,Spring Security 框架从数据库中查询认证信息
调用的就是这个接口中的方法,既然是框架作者规定好的,开发者照着规则
将信息封装到这个接口实现类中即可。。。本例使用的构造函数如下:
public User(
String username, 用户名
String password, 密码(如果密码没有加密,需要前缀识别,"{noop}")
boolean enabled, 用户是否可用?true表示能认证,反之不能!
boolean accountNonExpired, 账户没有过期?
boolean credentialsNonExpired, 证书没有过期?
boolean accountNonLocked, 账户没有被锁?
Collection<? extends GrantedAuthority> authorities 当前用户拥有的角色
GrantedAuthority 接口提供 getAuthority 抽象方法;框架提供了一个实现类
SimpleGrantedAuthority;将查询的角色封装到此对象,
然后丢给Spring Security 框架 就可以了
) */
User user = new User(
userInfo.getUsername(),
userInfo.getPassword(),
userInfo.getStatus() != 0,
true, true, true,
authorities);
return user;
}
/** 封装 角色 到Spring Security框架提供的实现类中! */
private List<SimpleGrantedAuthority> getAuthority(List<Role> roles) {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (Role role : roles) {
authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getRoleName()));
}
return authorities;
}
}
- 补充:UserDetails 接口源码
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
- 补充:UserDetails接口的实现类User 部分源码
public class User implements UserDetails, CredentialsContainer {
private String password;
private final String username;
private final Set<GrantedAuthority> authorities;
private final boolean accountNonExpired; //帐户是否过期
private final boolean accountNonLocked; //帐户是否锁定
private final boolean credentialsNonExpired; //认证是否过期
private final boolean enabled; //帐户是否可用
...
}
5). dao层代码
查询users表 以及多对多关联的 role 表,这个不是本文章描述的重点,用法和常规的dao层无异!
6). userServiceImpl新增用户的密码处理
/** 框架提供的加密工具类,每次加密底层都会产生一个动态的“盐”
* 每次加密的结果都不同,比md5加密方式更加安全;
* 虽然每次加密都不同,可以根据头信息计算出动态的盐,然后再进行计算比较(不是解密过程)*/
@Autowired
private BCryptPasswordEncoder passwordEncoder;
/** 插入一条用户信息,密码需要进行加密 */
@Override
public Integer saveUser(UserInfo userInfo) {
userInfo.setPassword(passwordEncoder.encode(userInfo.getPassword()));
return userDao.saveUser(userInfo);
}
二、服务器URI端动态权限控制【适合复杂的权限级别控制】
Spring Security框架提供了接口,在标签
<security:intercept-url ...
中使用特定SPEL即可实现
1). springSecurity.xml配置文件的配置变化
...
<!-- auto-config="true" 默认使用框架提供的简洁的登陆页面
use-expressions="false" 是否使用SPEL表达式,需要开启 -->
<security:http auto-config="true" use-expressions="true">
<!-- attern 拦截的规则,支持部分正则, access:SPEL表达式,调用permissionService类的hasPermission方法 -->
<security:intercept-url pattern="/**" access="@permissionService.hasPermission(request,authentication)" />
...
2). 上面SPEL表达式需要调用的接口
public interface PermissionService {
...
/** 动态权限验证 */
public Boolean hasPermission(HttpServletRequest request, Authentication authentication);
}
@Component("permissionService")
public class PermissionServiceImpl implements PermissionService {
...
/** 动态权限验证 */
@Override
public Boolean hasPermission(HttpServletRequest request, Authentication authentication) {
// 拿到域中的用户信息对象!
Object principal = authentication.getPrincipal();
// 要求为Spring Security框架提供的 User 接口实现类 UserDetails
if(principal instanceof UserDetails) {
// 获取 用户名 信息、注意,是用户名,不是角色名
String username = ((UserDetails)principal).getUsername();
// 当前用户可访问的所有的url
Set<String> urls = getUserPermission(username);
if (urls.contains(request.getRequestURI())) {
return true;
}
}
return false;
}
/**
模拟从数据库中根据对应的【username】拿到该用户
的角色,以及角色拥有的访问权限的【uri】;实际项目中,
需要将这些信息放在【redis】缓存中,查询时在redis中查找当对权限进行 增删改 时
需要同时维护 redis中的对应数据!这里只是简单的模拟一下!
*/
private Set<String> getUserPermission(String username) {
// 存放用户角色以及对应的uri
Map<String, Set<String>> map = new HashMap<>();
// 006 角色
Set<String> set006 = new HashSet<>();
set006.add("/product/findAll");
// 007 角色
Set<String> set007 = new HashSet<>();
set007.add("/order/findAll");
// 将角色信息加入map中
map.put("006", set006);
map.put("007", set007);
// 根据username 获取对应的 信息
Set<String> urls = map.get(username);
// 防止空指针异常
return urls == null ? new HashSet<>() : urls;
}
}
三、服务器端URI方法级权限控制【适合权限控制单一的场景】
在服务器端我们可以通过Spring security提供的注解对方法来进行权限控制。Spring Security在方法的权限控制上支持三种类型的注解,JSR-250注解、@Secured注解和支持表达式的注解,这三种注解默认都是没有启用的,需要单独通过global-method-security元素的对应属性进行启用
- 配置文件方式开启注解
<security:global-method-security
jsr250-annotations="enabled"
secured-annotations="enabled"
pre-post-annotations="enabled"/>
- (了解) 注解配置的Spring security框架的开启方式
@EnableGlobalMethodSecurity
:Spring Security默认是禁用注解的,要想开启注解,需要在继承WebSecurityConfigurerAdapter的类上加@EnableGlobalMethodSecurity注解,并在该类中将AuthenticationManager定义为Bean。
1). JSR-250注解的使用
- maven坐标
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>jsr250-api</artifactId>
<version>1.0</version>
</dependency>
- Spring Security 配置文件springSecurity.xml 注解开启
<!-- 服务器端方法级权限控制 -->
<security:global-method-security jsr250-annotations="enabled"/>
- 在需要被 过滤 的Controller中的方法 上添加注解
@RolesAllowed({"ADMIN"})
注解中的参数value是一个数组,可以省略 前缀 "ROLE_";当登陆的用户拥有列表中的权限,访问有效;
在之前用户登陆的时候,用户的认证信息是按照框架的接口规则将数据封装,框架会将当前的用户相关信息封装到session域中!
通过此session域,可以在过滤器(或者拦截器)中进行权限判断过滤!
@RolesAllowed({"ADMIN"})
@RequestMapping("/findAll")
public @ResponseBody ResponseMsg findAll() {...}
- 配置web.xml 403 forbidden
<!-- 配置权限不足的回馈页面 -->
<error-page>
<error-code>403</error-code>
<location>/error/403</location>
</error-page>
- 编写 Controller
@Controller
@RequestMapping("/error")
public class ErrorController {
@RequestMapping("/403")
public @ResponseBody ResponseMsg forbidden(){...}
...
- 补充(了解)
-
@PermitAll
: 表示允许所有的角色进行访问,也就是说不进行权限控制 -
@DenyAll
: 和PermitAll相反的,表示无论什么角色都不能访问
-
2). @Secured注解
使用方式与 JSR-250注解 类似,不过 @Secured 是 Spring Security框架提供的,不需要导入额外的maven坐标;
@Secured注解 不能省略前缀 "ROLE_"
- springSecurity.xml配置文件开启@Secured注解
<security:global-method-security secured-annotations="enabled"/>
- 在 Controller 的方法上使用注解
@Secured({"ROLE_ADMIN"})
@RequestMapping("/findAll")
public @ResponseBody ResponseMsg findAll(...)
- 其他步骤与JSR-250注解一样
3). 支持表达式的注解
同样是内置的,不需要额外导包;表达式 很多,需要时问度娘即可;
- 配置开启 springSecurity.xml
<security:global-method-security pre-post-annotations="enable"/>
- @PreAuthorize 使用举例
- 需要访问者拥有的权限
@PreAuthorize("hasRole('ROLE_ADMIN')")
@RequestMapping("/findAll")
public @ResponseBody ResponseMsg findAll(...)
- 指定特定用户能访问
@PreAuthorize("authentication.principal.username = '007'")
@RequestMapping("/saveProduct")
public @ResponseBody ResponseMsg saveProduct(Product product) {...}
- 组合表达式
@PreAuthorize("#userId == authentication.principal.userId or hasAuthority(‘ADMIN’)")
void changePassword(@P("userId") long userId ){...}
表示在changePassword方法执行之前,判断方法参数userId的值是否等于principal中保存的当前用户的
userId,或者当前用户是否具有ROLE_ADMIN权限,两种符合其一,就可以访问该方法。
- 补充
- @PostAuthorize 允许方法调用,但是如果表达式计算结果为false,将抛出一个安全性异常
@PostAuthorize
User getUser("returnObject.userId == authentication.principal.userId or
hasPermission(returnObject, 'ADMIN')");
@PostFilter 允许方法调用,但必须按照表达式来过滤方法的结果
@PreFilter 允许方法调用,但必须在进入方法之前过滤输入值
Spring Security允许我们在定义URL访问或方法访问所应有的权限时使用Spring EL表达式,在定义所需的访问权限时如果对应的表达式返回结果为true则表示拥有对应的权限,反之则无。Spring Security可用表达式对象的基类是SecurityExpressionRoot,其为我们提供了如下在使用Spring EL表达式对URL或方法进行权限控制时通用的内置表达式。
表达式 | 描述 |
---|---|
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。 |
四、页面端标签控制权限(了解)
在jsp页面中我们可以使用spring security提供的权限标签来进行权限控制;相关的表达式可以去度娘学习参考
1). 准备
- 注意:需要在 springSecurity.xml中配置bean
<bean id="webSecurityExpressionHandler" class="org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler"/>
- 导入maven坐标
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>{version}</version>
</dependency>
- jsp页面导入对应的taglib
<%@taglib uri="http://www.springframework.org/security/tags" prefix="security"%>
1). 三种标签使用介绍
在jsp中我们可以使用以下三种标签,其中authentication代表的是当前认证对象,可以获取当前认证对象信息,例如用户名。其它两个标签我们可以用于权限控制
1. authentication
<security:authentication property="" htmlEscape="" scope="" var=""/>
-
property
:只允许指定Authentication所拥有的属性,可以进行属性的级联获取,如principle.username
,不允许直接通过方法进行调用 -
htmlEscape
:表示是否需要将html进行转义。默认为true。 -
scope
:与var属性一起使用,用于指定存放获取的结果的属性名的作用范围,默认pageContext。Jsp中拥有的作用范围都进行进行指定 -
var
: 用于指定一个属性名,这样当获取到了authentication的相关信息后会将其以var指定的属性名进行存放,默认是存放在pageConext中
2. authorize
authorize是用来判断普通权限的,通过判断用户是否具有对应的权限而控制其所包含内容的显示
<security:authorize access="" method="" url="" var="">...</security:authorize>
-
access
: 需要使用表达式来判断权限,当表达式的返回结果为true时表示拥有对应的权限 -
method
:method属性是配合url属性一起使用的,表示用户应当具有指定url指定method访问的权限,method的默认值为GET,可选值为http请求的7种方法 -
url
:url表示如果用户拥有访问指定url的权限即表示可以显示authorize标签包含的内容 -
var
:用于指定将权限鉴定的结果存放在pageContext的哪个属性中
3. accesscontrollist
accesscontrollist标签是用于鉴定ACL权限的。其一共定义了三个属性:hasPermission、domainObject和var,其中前两个是必须指定的
<security:accesscontrollist hasPermission="" domainObject="" var=""></security:accesscontrollist>
-
hasPermission
:hasPermission属性用于指定以逗号分隔的权限列表 -
domainObject
:domainObject用于指定对应的域对象 -
var
:var则是用以将鉴定的结果以指定的属性名存入pageContext中,以供同一页面的其它地方使用
五、补充
1). 配置文件中将sercurity配置设置为默认
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<!-- beans:beans 让 beans这个标签带上 beans: 前缀
xmlns=.../security 这种配置将security相关的标签设置为默认
这样可以不写 <security:xxx 这种前缀了 -->
</beans:beans>
2). 密码修改业务中需要使用的方法
验证密码是否正确
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
boolean matches = passwordEncoder.matches("原始密码", "被加密后的密码");
3). IS_AUTHENTICATED_ANONYMOUSLY匿名用户
access="IS_AUTHENTICATED_ANONYMOUSLY" 用于设置资源可以在不登陆时可以访问。此配置与 security="none"的区别在于当用户未登陆时获取登陆人账号的值为anonymousUser ,而security="none"的话,无论是否登陆都不能获取登录人账号的值。
...
<security:http auto-config="true" use-expressions="false">
<!-- 【【【】】】Spring Security框架中 提供的默认用户!不需要权限,直接获得Security的通过许可,
而且为起创建 匿名用户 对象;使用此配置的目的是向让用户在未认证的前提下使用安全框架的功能,
比如 获取用户名等 -->
<security:intercept-url pattern="/cart/*.do" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
<security:intercept-url pattern="/**" access="ROLE_USER,ROLE_ADMIN" />
...