SpringSecurity
简介
使用方式(这里用第三种方式)
-
一种是全部利用配置文件,将用户、权限、资源(url)硬编码在xml文件中
-
二种是用户和权限用数据库存储,而资源(url)和权限的对应采用硬编码配置
-
三种是细分角色和权限,并将用户、角色、权限和资源均采用数据库存储,并且自定义过滤器,代替原有的FilterSecurityInterceptor过滤器, 并分别实现AccessDecisionManager、InvocationSecurityMetadataSourceService和UserDetailsService,并在配置文件中进行相应配置。
SpringSecurity-HelloWorld
测试环境搭建
pom配置
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.3.20.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
XML配置
<servlet>
<servlet-name>springDispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springDispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
Spring配置
<?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:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/mvc
<u>http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd</u>
<u>http://www.springframework.org/schema/beans</u>
<u>http://www.springframework.org/schema/beans/spring-beans.xsd</u>
<u>http://www.springframework.org/schema/context</u>
<u>http://www.springframework.org/schema/context/spring-context-4.3.xsd</u>">
<context:component-scan base-package="com.atguigu.security"></context:component-scan>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
<mvc:annotation-driven />
<mvc:default-servlet-handler />
</beans>
引入SpringSecurity框架
添加security-pom依赖
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>4.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>4.2.10.RELEASE</version>
</dependency>
<!-- 标签库 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>4.2.10.RELEASE</version>
</dependency>
web.xml中添加SpringSecurity的Filter进行安全控制
<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>
加入SpringSecurity配置类
@Configuration、@Bean 注解作用
@Configuration
@EnableWebSecurity
public class AppWebSecurityConfig extends WebSecurityConfigurerAdapter {
}
SpringSecurity-实验
授权首页和静态资源
配置类(AppWebSecurityConfig extends WebSecurityConfigurerAdapter)
重写configure(HttpSecurity http)方法
@Configuration
@EnableWebSecurity
public class AppWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//super.configure(http); //取消默认配置
http.authorizeRequests()
.antMatchers("/layui/**","/index.jsp").permitAll() //设置匹配的资源放行
.anyRequest().authenticated(); //剩余任何资源必须认证
}
}
静态资源和index.jsp都可以访问
不存在的资源
有权限无资源404
默认及自定义登录页
开启formLogin()功能 默认主页时
静态资源和index.jsp都可以访问
自定义表单登录逻辑分析
表单提交地址:${PATH }/index.jsp
表单提交请求方式:post
表单提交请求失败,提取错误消息:${SPRING_SECURITY_LAST_EXCEPTION.message}
自定义认证用户信息
auth.inMemoryAuthentication()
.withUser("zhangsan").password("123456").roles("ADMIN")
.and()
.withUser("lisi").password("123123").authorities("USER","MANAGER");
用户注销完成
添加注销功能(logout)http.logout()默认规则
/logout:退出系统
如果csrf开启,必须post方式的/logout请求,表单中需要增加csrf token
logoutUrl();退出系统需要发送的请求
logoutSuccessUrl();退出系统成功以后要跳转的页面地址
addLogoutHandler():自定义注销处理器
deleteCookies():指定需要删除的cookie
invalidateHttpSession():session失效(DEBUG)
基于角色的访问控制
//设置资源可以访问的角色
http.authorizeRequests().antMatchers("/layui/**","/index.jsp").permitAll() //允许所有人都访问静态资源,无论登录(认证)与否
.antMatchers("/level1/**").hasRole("学徒")
.antMatchers("/level2/**").hasRole("大师")
.antMatchers("/level3/**").hasRole("宗师")
.anyRequest().authenticated(); //放置最后,以上没有规定的都需要权限认证。
//设置拥有该角色的资源可以访问
auth.inMemoryAuthentication()
.withUser("zhangsan").password("123456").roles("ADMIN","学徒","宗师")
.and()
.withUser("自定义访问拒绝处理页面,lisi").password("111111").authorities("USER","MANGER");
自定义访问拒绝处理页面
http.exceptionHandling().accessDeniedPage("/unauth.html");
<%@ page language="java" contentType="text/html;
charset=UTF-8"
pageEncoding="UTF-8"%>
<%
pageContext.setAttribute("PATH",
request.getContextPath());
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport"
content="width=device-width, initial-scale=1,
maximum-scale=1">
<title>武林秘籍管理系统</title>
<link rel="stylesheet" href="${PATH
}/layui/css/layui.css">
</head>
<body class="layui-layout-body">
<div class="layui-layout layui-layout-admin">
<!-- 顶部导航 -->
<%@include file="/WEB-INF/include/navbar.jsp" %>
<!-- 侧边栏 -->
<%@include file="/WEB-INF/include/sidebar.jsp"
%>
<div class="layui-body">
<!-- 内容主体区域 -->
<div style="padding: 15px;">
<h1>你无权访问</h1>
</div>
</div>
<div class="layui-footer"></div>
</div>
<script src="${PATH }/layui/layui.js"></script>
<script src="${PATH
}/layui/jquery-2.1.1.min.js"></script>
<script>
//JavaScript代码区域
layui.use('element', function() {
var element = layui.element;
});
</script>
</body>
</html>
http.exceptionHandling().accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
request.setAttribute("message", accessDeniedException.getMessage());
request.getRequestDispatcher("/WEB-INF/views/unauth.jsp").forward(request, response);
}
});
记住我功能
简单版本
http.rememberMe();
数据库版本
- 引入pom.xml包
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>4.3.20.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.12</version>
</dependency>
<!-- mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
- 配置数据源
<!-- 配置数据源 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="username" value="root"></property>
<property name="password" value="root"></property>
<property name="url" value="jdbc:mysql://192.168.137.3:3306/security?useSSL=false"></property>
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
</bean>
<!-- jdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
-
创建表
-
设置记住我
<!-- 配置数据源 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="username" value="root"></property>
<property name="password" value="root"></property>
<property name="url" value="jdbc:mysql://192.168.137.3:3306/security?useSSL=false"></property>
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
</bean>
<!-- jdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
认证
自定义UserDetailsService检索用户
package com.atguigu.security.component;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.ColumnMapRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import com.alibaba.druid.util.StringUtils;
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
JdbcTemplate jdbcTemplate;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String queryUser = "SELECT * FROM `t_admin` WHERE loginacct=?";
//1、查询指定用户的信息
Map<String, Object> map = jdbcTemplate.queryForMap(queryUser, username);
//查询用户拥有的角色集合
String sql1="SELECT t_role.* FROM t_role LEFT JOIN t_admin_role ON t_admin_role.roleid=t_role.id WHERE t_admin_role.adminid=?";
List<Map<String, Object>> roleList = jdbcTemplate.query(sql1, new ColumnMapRowMapper(), map.get("id"));
System.out.println("roleList="+roleList);
//查询用户拥有的权限集合
String sql2 = "SELECT distinct t_permission.* FROM t_permission LEFT JOIN t_role_permission ON t_role_permission.permissionid = t_permission.id LEFT JOIN t_admin_role ON t_admin_role.roleid=t_role_permission.roleid WHERE t_admin_role.adminid=?";
List<Map<String, Object>> permissionList = jdbcTemplate.query(sql2, new ColumnMapRowMapper(), map.get("id"));
System.out.println("permissionList="+permissionList);
//用户权限=【角色+权限】
Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
for (Map<String, Object> rolemap : roleList) {
String rolename = rolemap.get("name").toString();
authorities.add(new SimpleGrantedAuthority("ROLE_"+rolename));
}
for (Map<String, Object> permissionmap : permissionList) {
String permissionName = permissionmap.get("name").toString();
if(!StringUtils.isEmpty(permissionName)) {
authorities.add(new SimpleGrantedAuthority(permissionName));
}
}
System.out.println("authorities="+authorities);
//return new User(map.get("loginacct").toString(),map.get("userpswd").toString(),
//AuthorityUtils.createAuthorityList("ADMIN","USER"));
return new User(map.get("loginacct").toString(),map.get("userpswd").toString(),authorities);
}
自定义实现类---》Dao层(调用实现类)--》数据库查询的封装到Userdetails对象中
页面提交的账号密码封装到Authentication对象中
将密码加密后与数据库中密码进行比对
基于数据库(MD5密码)认证 (debug)
配置configure
//测试:分析源码(验证密码不一致)
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
引入MD5加密工具
PasswordEncoder接口实现类
package com.atguigu.security.component;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@Component
public class PasswordEncoderImpl implements PasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
// TODO Auto-generated method stub
return MD5Util.digest(rawPassword.toString());
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
// TODO Auto-generated method stub
return encodedPassword.equals(MD5Util.digest(rawPassword.toString()));
}
}
基于数据库(BCryptPasswordEncoder)密码加密认证
细粒度权限控制
前置细节【Role和Authority的区别】
用户拥有的权限表示
roles("ADMIN","学徒","宗师")
authorities("USER","MANAGER");
给资源授予权限(角色或权限)
//.antMatchers("/level1/**").hasRole("学徒")
//.antMatchers("/level1/**").hasAnyRole("学徒","ADMIN")//拥有任何一个角色都可以访问
.antMatchers("/level1/**").hasAnyAuthority("学徒","ADMIN") //拥有任何一个权限都可以访问
.antMatchers("/level2/**").hasRole("大师")
.antMatchers("/level3/**").hasRole("宗师")
roles("ADMIN","学徒","宗师")
增加"ROLE_"前缀存放:【"ROLE_ADMIN","ROLE_学徒","ROLE_宗师"】
表示拥有的权限。一个角色表示的是多个权限
用户传入的角色不能以ROLE_开头,否则会报错。ROLE_是自动加上的
如果我们保存的用户的角色:直接传入角色的名字,权限【new
SimpleGrantedAuthority("ROLE_" + role)】保存即可
authorities("USER","MANAGER");
原样存放:【"USER","MANAGER"】
表示拥有的权限。
如果我们保存的是真正的权限;直接传入权限名字,权限【new SimpleGrantedAuthority(role)】保存
无论是Role还是Authority都保存在 List<GrantedAuthority>,每个用户都拥有自己的权限集合->List<GrantedAuthority>
细粒度的资源控制
authenticated():通过认证的用户都可以访问
permitAll():允许所有人访问,即使未登录
authorizeRequests():更细粒度的控制
access(String): SpEL:Spring表达式
.access("hasRole('大师') AND hasAuthority('user:delete') OR hasIpAddress('192.168.50.15')")
细粒度的资源控制相应注解
@EnableWebSecurity:开启 Spring Security 注解
@EnableGlobalMethodSecurity(prePostEnabled=true):开启全局的细粒度方法级别权限控制功能
@PreAuthorize("hasRole('ADMIN')")
public void addUser(User user){
//如果具有ROLE_ADMIN 权限 则访问该方法
....
}
@PostAuthorize:允许方法调用,但是,如果表达式结果为false抛出异常
//returnObject可以获取返回对象user,判断user属性username是否和访问该方法的用户对象的用户名一样。不一样则抛出异常。
@PostAuthorize("returnObject.user.username==principal.username")
public User getUser(int userId){
//允许进入
...
return user;
}
//将结果过滤,即选出性别为男的用户
@PostFilter("returnObject.user.sex=='男' ")
public List<User> getUserList(){
//允许进入
...
return user;
}
@PreFilter:允许方法调用,但必须在进入方法前过滤输入值
@Secured('ADMIN') 等价于 @PreAuthorize("hasRole('ADMIN')")
细粒度权限控制实现步骤
https://docs.spring.io/spring-security/site/docs/4.0.1.RELEASE/reference/htmlsingle/#el-common-built-in
开启全局的细粒度方法级别权限控制功能
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
@Configuration
public class AppSecurityConfig extends WebSecurityConfigurerAdapter {
}
将手动授权的方式注释掉
//.antMatchers("/level1/**").hasRole("学徒")
//.antMatchers("/level1/**").hasAnyRole("学徒","ADMIN")
//.antMatchers("/level1/**").hasAnyAuthority("学徒","ADMIN")
//.antMatchers("/level1/**").hasAuthority("学徒")
//.antMatchers("/level2/**").hasRole("大师")
//.antMatchers("/level3/**").hasRole("宗师")
给访问资源的方法增加注解,进行访问授权
package com.atguigu.security.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@Controller
public class GongfuController {
@PreAuthorize("hasRole('学徒') AND hasAuthority('luohan')")
@GetMapping("/level1/1")
public String leve1Page(){
return "/level1/1";
}
@PreAuthorize("hasRole('学徒') AND hasAuthority('wudang')")
@GetMapping("/level1/2")
public String leve2Page(){
return "/level1/2";
}
@PreAuthorize("hasRole('学徒') AND hasAuthority('quanzhen')")
@GetMapping("/level1/3")
public String leve3Page(){
return "/level1/3";
}
}
通过数据库加载用户权限
package com.atguigu.security.component;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.ColumnMapRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import com.alibaba.druid.util.StringUtils;
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
JdbcTemplate jdbcTemplate;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String queryUser = "SELECT * FROM `t_admin` WHERE loginacct=?";
//1、查询指定用户的信息
Map<String, Object> map = jdbcTemplate.queryForMap(queryUser, username);
//查询用户拥有的角色集合
String sql1="SELECT t_role.* FROM t_role LEFT JOIN t_admin_role ON t_admin_role.roleid=t_role.id WHERE t_admin_role.adminid=?";
List<Map<String, Object>> roleList = jdbcTemplate.query(sql1, new ColumnMapRowMapper(), map.get("id"));
System.out.println("roleList="+roleList);
//查询用户拥有的权限集合
String sql2 = "SELECT distinct t_permission.* FROM t_permission LEFT JOIN t_role_permission ON t_role_permission.permissionid = t_permission.id LEFT JOIN t_admin_role ON t_admin_role.roleid=t_role_permission.roleid WHERE t_admin_role.adminid=?";
List<Map<String, Object>> permissionList = jdbcTemplate.query(sql2, new ColumnMapRowMapper(), map.get("id"));
System.out.println("permissionList="+permissionList);
//用户权限=【角色+权限】
Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
for (Map<String, Object> rolemap : roleList) {
String rolename = rolemap.get("name").toString();
authorities.add(new SimpleGrantedAuthority("ROLE_"+rolename));
}
for (Map<String, Object> permissionmap : permissionList) {
String permissionName = permissionmap.get("name").toString();
if(!StringUtils.isEmpty(permissionName)) {
authorities.add(new SimpleGrantedAuthority(permissionName));
}
}
System.out.println("authorities="+authorities);
//return new User(map.get("loginacct").toString(),map.get("userpswd").toString(),
//AuthorityUtils.createAuthorityList("ADMIN","USER"));
return new User(map.get("loginacct").toString(),map.get("userpswd").toString(),authorities);
}
}
准备数据
查询用户拥有的角色集合
SELECT
t_role.*
FROM
t_role
LEFT JOIN t_admin_role
ON t_admin_role.roleid = t_role.id
WHERE t_admin_role.userid = 1
查询用户拥有的权限集合
SELECT DISTINCT
t_permission.*
FROM
t_permission
LEFT JOIN t_role_permission
ON t_role_permission.permissionid =
t_permission.id
LEFT JOIN t_admin_role
ON t_admin_role.roleid =
t_role_permission.roleid
WHERE t_admin_role.userid = 1
SpringSecurity-原理
初始化方法
过滤器:功能扩展的多个过滤器->责任链设计模式
获取过滤器链中的过滤器,封装为虚拟的VirtualFilterChain对象,并开始执行过滤
开始一个一个的执行过滤器
不同过滤器介绍(直接看文档)
UsernamePasswordAuthenticationFilter认证原理
执行过滤器,获取到页面的用户名和密码
将username和password包装成UsernamePasswordAuthenticationToken
获取系统的认证管理器(AuthenticationManager)来调用authenticate方法完成认证(this.getAuthenticationManager().authenticate(authRequest))
AuthenticationManager获取ProviderManager来调用ProviderManager.authenticate()
ProviderManager获取到所有的AuthenticationProvider判断当前的提供者能否支持,如果支持provider.authenticate(authentication);
DaoAuthenticationProvider( authentication :页面封装用户名和密码的对象)
3.2.1)、retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
3.2.1.1)、 loadedUser = this.getUserDetailsService().loadUserByUsername(username);
(调用我们自己的UserDetailsService来去数据库查用户,按照用户名查出来的用户的详细信息)封装成UserDetail
3.2.2)、 preAuthenticationChecks.check(user);(预检查,账号是否被锁定等…)
3.2.3)、 additionalAuthenticationChecks(附加的认证检查)
3.2.3.1)、使用passwordEncoder. matches检查页面的密码和数据库的密码是否一致
3.2.4)、 postAuthenticationChecks.check(user);(后置认证,检查密码是否过期)
3.2.5)、 createSuccessAuthentication:将认证成功的信息重新封装成UsernamePasswordAuthenticationToken
3.3)、 3.2又返回了一个新的UsernamePasswordAuthenticationToken,然后擦掉密码
eventPublisher.publishAuthenticationSuccess(result);认证成功
successfulAuthentication(request, response, chain, authResult);
通过调用 SecurityContextHolder.getContext().setAuthentication(...) 将 Authentication 对象赋给当前的 SecurityContext
认证流程
用户使用用户名和密码登录
用户名密码被过滤器(默认为 UsernamePasswordAuthenticationFilter)获取到,封装成
Authentication(UsernamePasswordAuthenticationToken)
token(Authentication实现类)传递给 AuthenticationManager 进行认证
AuthenticationManager 认证成功后返回一个封装了用户权限信息的 Authentication 对象
通过调用 SecurityContextHolder.getContext().setAuthentication(...) 将 Authentication 对象赋给当前的 SecurityContext
将用户的信息保存到当前线程上,共享起来
SecurityContextHolder.getContext();就能获取到之前认证好的Authentication对象(UsernamePasswordAuthenticationToken