文章目录
前言
认证:
身份验证是验证用户身份的过程。也就是说,当用户通过应用程序进行身份验证时,他们在证明自己实际上就是他们所说的身份。
授权:
授权本质上是访问控制-控制用户可以在应用程序中访问的内容(例如资源,网页等)。大多数用户通过使用角色和权限等概念来执行访问控制。也就是说,通常根据分配给他们的角色和/或权限,允许用户执行某项操作或不执行某项操作。
一、Spring Security
Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架。它是保护基于 Spring 的应用程序的事实标准。
Spring Security 是一个专注于为 Java 应用程序提供身份验证和授权的框架。像所有 Spring 项目一样,Spring Security 的真正强大之处在于它可以轻松扩展以满足自定义需求
为Spring IO Platform提供安全服务
二、特征
- 对身份验证和授权的全面且可扩展的支持(认证 授权 )
- 防止会话固定、点击劫持、跨站点请求伪造等攻击(安全)
- Servlet API 集成
- 与 Spring Web MVC 的可选集成
三、spring security 和 shrio的区别
相同点
1:认证功能
2:授权功能
3:加密功能
4:会话管理
5:缓存支持
6:rememberMe功能
不同点
优点:
- spring Security基于spring 开发 如果是spring项目 配合spring security做权限根据更加方便 ,而shrio 需要和 spring进行整合开发
- spring Security 功能比Shiro更加丰富些 ,例如安全防护
- Spring Security 社区资源比shiro丰富
缺点 - shrio配置和使用比较简单,spring Security 比较复杂
- shrio依赖性低 不需要任何框架和容器,可以独立运行 ,而 spring Security 依赖于spring容器
四、实例
入门示例
- 创建springboot项目 选择依赖
<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.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</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>
- 编写一个类
package com.aaa.sbs.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author zhangyifan
* @version 8.0
* @description:
* @date 2022/2/14 9:05
*/
@RestController
@RequestMapping("hello")
public class HelloController {
/**
* 入门测试
* @return
*/
@GetMapping("helloSpringSecurity")
public String hello(){
return "hello Security";
}
}
一个方法就行
- 直接访问这个方法
http://localhost:18888/hello/helloSpringSecurity
加入security包后就需要认证后才可以访问,会跳转到自带登录页面
使用user 和 控制台打印的随机密码登录
4. 登录原码解析(使用debug)
当我们没有认证时,会请求一个默认过滤器DefaultLoginPageGeneratingFilter,
查看doFilter方法可以看到,如果登录成功直接放行,如果没有登录会调用generateLoginPageHtml(request, loginError, logoutSuccess)方法,判断this.formLoginEnabled就会看到登录界面(在servlet中学习过)
当点击登录时,会再经过一个过滤器UsernamePasswordAuthenticationFilter,执行它中的(直接双击shift搜索)
进行认证,当认证成功会调用该过滤器的父类AbstractAuthenticationProcessingFilter中的successfulAuthentication方法,失败时会调用unsuccessfulAuthentication方法。
认证时系统默认的密码会通过UserDetailsServiceAutoConfiguration中的getOrDeducePassword获取或者推断密码方法打印到控制台,从中可以看出在SecurityProperties类中有一个内部类User,我们登录的用户名和密码都是通过该实体产生。
自定义用户名和密码
- 在application.properties配置文件中配置:
spring.security.user.name=admin
spring.security.user.password=tiger
- 使用配置类
package com.aaa.sbs.config;
import lombok.extern.log4j.Log4j2;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author zhangyifan
* @version 8.0
* @description:
* @date 2022/2/14 10:33
*/
@Configuration
@Log4j2
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//实例化 BCryptPasswordEnder
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
//使用加密类生成加密密码
String password = bCryptPasswordEncoder.encode("tiger");
log.info("加密后的密码"+password);
boolean matchesPassword = bCryptPasswordEncoder.matches("tiger",password);
log.info("密码是否正确"+matchesPassword);
//等于吧用户名密码直接配置 。使用时加载到内存中
//withUser配置 用户名 scott 密码 tiger
auth.inMemoryAuthentication().withUser("scott").password(password).roles("guanliyuan");
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
- 使用配置类加UserDetailsService(常用):
- 配置类
package com.aaa.sbs.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import javax.annotation.Resource;
/**
* @author zhangyifan
* @version 8.0
* @description:
* @date 2022/2/14 10:32
*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 配置获取用户信息接口 配置加密方式
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
- 服务类
package com.aaa.sbs.service;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
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.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @author zhangyifan
* @version 8.0
* @description: 相当与 shrio的realm
* @date 2022/2/14 10:34
*/
@Service
public class WebDetailServiceImpl implements UserDetailsService {
//注入userService接口
/*
* private UserService userService */
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
List<GrantedAuthority> grantedAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList("guanliyuan,dailishang,shanghu,dept:query,dept:update");
//第一个参数:数据库取出用户名 第二个参数:加密密码 第三个参数:角色/权限集合
return new User("root",new BCryptPasswordEncoder().encode("tiger"),grantedAuthorities);
}
}
关键类
-
UsernamePasswordAuthenticationFilter
对登录信息进行拦截,检查校验表单中的用户名和密码的一个过滤器。
attemptAuthentication 认证方法
successfulAuthentication 认证成功方法
unsuccessfulAuthentication 认证失败方法 -
UserDetailsService
查询数据库数据的一个接口,返回一个UserDetails,UserDetails的子类User就是我们要使用的用户信息类(包含用户名,密码,权限信息等)
方法
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; -
PasswordEncoder
是Spring Security提供的密码加密方式的接口定义。
String encode(CharSequence rawPassword);
用来对明文密码进行加密,返回加密之后的密文。
//一个密码校对方法,在用户登录的时候,将用户传来的明文密码和数据库中保存的密文密码作为参数,传入到这个方法中去,根据返回的Boolean 值判断用户密码是否输入正确。
boolean matches(CharSequence rawPassword, String encodedPassword);
//如果解析的密码能够再次进行解析且达到更 安全的结果则返回 true,否则返回 false。默认返回 false。
default boolean upgradeEncoding(String encodedPassword) { return false; } -
Spring Security 提供了多种密码加密方案,
官方推荐使用 BCryptPasswordEncoder,BCryptPasswordEncoder 使用 BCrypt 强哈希函数,开发者在使用时可以选择提供 strength 和 SecureRandom 实例。strength 越大,密钥的迭代次数越多,密钥迭代次数为 2^strength。strength 取值在 4~31 之间,默认为 10。
不同于 Shiro 中需要自己处理密码加盐,在 Spring Security 中,BCryptPasswordEncoder 就自带了盐,处理起来非常方便。
package com.aaa.sbs.test;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author zhangyifan
* @version 8.0
* @description: 测试BCryptPasswordEncoder
* @date 2022/2/14 15:37
*/
public class PasswordEncoderTest {
public static void main(String[] args) {
PasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
//加密密码
String password = bCryptPasswordEncoder.encode("tiger");
System.out.println("密码"+password);
//验证密码是否正确
boolean isSuc = bCryptPasswordEncoder.matches("tiger",password);
System.out.println("密码是否正确"+isSuc);
boolean isSucl = bCryptPasswordEncoder.matches("tiger","$2a$10$kkB4WomyMeYRhg.iC6g0OubsrZGBvAfX7Cuq0mtDkIbMUpLTahDpy");
System.out.println("比对过去生成的密码是否正确"+isSucl);
}
}
自定义登录页面,用户登出和未认证授权的错误页面配置(403)
- 配置 WebSecuityConfig
package com.aaa.sbs.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import javax.annotation.Resource;
/**
* @author zhangyifan
* @version 8.0
* @description:
* @date 2022/2/14 10:32
*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 配置获取用户信息接口 配置加密方式
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 自定义用户登录,注销 角色 /权限 授权配置 都在该方法配置
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置登录页面信息
http.formLogin()//总体form方式
.loginPage("/html/login.html") //配置登录页面路劲
.loginProcessingUrl("/user/login") //登录页面中form 配置的请求地址
.failureUrl("/html/login.html?error")//登录失败路劲配置
.defaultSuccessUrl("/html/indexl.html").permitAll()//默认登录成功的
.and().authorizeRequests()//逻辑所有的授权请求
.antMatchers("/user/login","/","/css/**","/js/**").permitAll()
.anyRequest().authenticated()//除了上面配置的,其他请求都需要认证
.and().csrf().disable();//关闭csrf概念 如果不关闭
//配置未授权跳转页面
http.exceptionHandling().accessDeniedPage("/html/unauthorized.html");
//注销配置 如果使用默认 /logout 可以不用配置
http.logout().logoutUrl("/logout")//用户请求注销 请求地址
.logoutSuccessUrl("/html/login.html").permitAll();//注销成功跳转地址
}
}
- 登录页面和未授权页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>员工登录</title>
<script>
function load(){
var url = location.href;
//判断url中是否含有?
if(url.indexOf("?")!=-1){
// 原始js jquery
//原始JS赋值 <a>url</ a> .innerText .text()
// input .value= .val()
// div .innerHTML .html
document.getElementById("errorInfo").innerHTML="用户名或者密码错误!!";
}
}
</script>
</head>
<body onload="load()">
<center>
<h3>登录页面</h3>
<form action="/user/login" method="post">
<div id="errorInfo" style="color: red"></div>
<table border="1" >
<!--用户名和密码的name一定是 username password 大小写也有区分-->
<tr><td>用户名</td><td><input type="text" name="username"> </td></tr>
<tr><td>密码</td><td><input type="text" name="password"> </td></tr>
<tr><td colspan="2" align="center"><input type="submit" value="登录"></td></tr>
</table>
</form>
</center>
</body>
</html>
unauthorized.html(注意页面名称中不能含有-否则404)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>错误页面</title>
</head>
<body>
访问了未授权的地址。。。。。。。。。。。
</body>
</html>
- 测试
n:root
p:tiger
直接访问刚才的hello方法
直接访问,看是否访问了已经访问的页面
先请求登录
在另外窗口退出
再访问原来页面,发现需要登录,说明已经退出
授权操作
1. hasAuthority(有权限)和 hasAnyAuthority (有任何权限)用法
编写模拟控制器
@RestController
@RequestMapping("dept")
public class DeptController {
/**
* 模拟部门查询
* @return
*/
@GetMapping("queryAll")
public String queryAll(){
return "模拟部门查询";
}
}
授权配置
服务配置
@Service
public class WebDetailServiceImpl implements UserDetailsService {
//注入userService接口
/*
* private UserService userService */
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
List<GrantedAuthority> grantedAuthorities =
AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_guanliyuan,ROLE_dailishang,shanghu,dept:query,dept:update");
//第一个参数:数据库取出用户名 第二个参数:加密密码 第三个参数:角色/权限集合
return new User("root",new BCryptPasswordEncoder().encode("tiger"),grantedAuthorities);
}
}
如果配置拥有某一个或者在多个权限中任意一个,就可以访问,否则不可以
hasRole(有角色)和hasAnyRole(有任何角色)方法
- 模拟控制器
/**
* 根据编号查部门
* @param deptNo
* @return
*/
@GetMapping("queryById")
public String queryById(Integer deptNo){
return "根据编号查询部门";
}
- 权限配置
// .antMatchers("/dept/queryById").hasRole("guanliyuan")//拥有管理角色访问该路径
.antMatchers("/dept/queryById").hasAnyRole("guanliyuan","dailishang")//设置拥有多个中某一个角色,才可以访问
- 角色
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
List<GrantedAuthority> grantedAuthorities =
AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_guanliyuan,ROLE_dailishang,shanghu,dept:query,dept:update");
//第一个参数:数据库取出用户名 第二个参数:加密密码 第三个参数:角色/权限集合
return new User("root",new BCryptPasswordEncoder().encode("tiger"),grantedAuthorities);
}
测试
http://localhost:18888/dept/queryById?deptNo=1
hasIpAddress
- 编写模拟控制器
@GetMapping("add")
public String add(){
return "模拟部门添加";
}
- 配置
.antMatchers("/dept/add").hasIpAddress("192.168.220.1")//固定的ip才允许
http://localhost:18888/dept/add
http://192.168.220.1:18888/dept/add(正确)
授权注解
@Secured
验证当前用户是否具备某一角色,拥有可以访问,否则不可以。
注意
当@EnableGlobalMethodSecurity(securedEnabled=true)的时候,@Secured可以使用
/**
* 模拟用户查询
* @return
*/
//这两个有任意一个就可以访问
@Secured({"ROLE_guanliyuan","ROLE_dailishang"})
@GetMapping("queryAll")
public String queryAll(){
return "模拟用户查询";
}
说明:拥有商户或者代理商角色的用户都可以方法queryAll方法。另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_“。
如果我们要求,只有同时拥有商户和代理商的用户才能方法queryAll()方法,这时候@Secured就无能为力了。
修改UserDetailServiceImpl 中的AuthorityUtils.commaSeparatedStringToAuthorityList(“dept:update,ROLE_dailishang,ROLE_shanghu”);进行测试
@ PreAuthorize
该注解在方法执行之后进行校验。
@PostAuthorize 注解使用并不多,在方法执行后再进行权限验证,适合验证带有返回值的权限,Spring EL 提供 返回对象能够在表达式语言中获取返回的对象returnObject。当@EnableGlobalMethodSecurity(prePostEnabled=true)的时候,@PostAuthorize可以使用:
//拥有一个权限就可以
//@PreAuthorize("hasRole('ROLE_guanliyuan')")
//两个必须都有
//@PreAuthorize("hasRole('ROLE_guanliyuan') and hasRole('ROLE_dailishang')")
//任意拥有一个就可以
//@PreAuthorize("hasAnyRole('ROLE_guanliyuan','ROLE_dailishang')")
//该方法可以通过returnObject获取到返回值做判断
@PostAuthorize("returnObject!=null")
@GetMapping("add")
public String add(){
return "模拟用户添加";
}