赞
踩
基于Spring 框架,提供了一套Web 应用安全性的完整解决方案。
关于安全方面的主要两个区域是 “认证” 和 “授权” (或者访问控制),一般来说,Web应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分,这两点是 Spirng Security 重要核心功能。
使用spring boot 快速创建工具创建一个boot工程
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.4.5</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.kanan</groupId> <artifactId>secruity-demo-1</artifactId> <version>0.0.1-SNAPSHOT</version> <name>security-demo-1</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("hello")
public String hello(){
return "Hello Security";
}
}
localhost:80/test/hello //进行访问
打开了一个登录页面,表示开启成功
默认的用户名:user
默认密码:每次启动时,在控制台会生成一个随机密码
Using generated security password: a2364a8b-5504-43d4-9ab4-93d690fe264b
登录成功后就可以看的我们的 hello security
代码底层流程:重点有三个过滤器
**FilterSecurityInterceptor:**是一个方法级的权限过滤器,基本位于过滤器链的最底层
super.beforeInvocation(filterInvocation):表示查看之前的filter 是否通过
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse()):表示真正调用后台的服务
**ExceptionTranslationFilter:**是一个异常过滤器,用来处理在认证授权过程中抛出的异常
**UsernamePasswordAuthenticationFilter:**对/login的POST请求做拦截,校验表单中用户名,密码。
当我们没有进行配置时,账号和密码是由Spring Security定义生成的。而在实际项目中账号和密码应该都是从数据库中查询出来的。所以我们要通过自定义逻辑控制认证逻辑。
使用 yml 配置文件
spring:
security:
user:
name: kanan
password: 123456
@Configuration public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //创建一个加密类 BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); String password = passwordEncoder.encode("123456"); auth.inMemoryAuthentication().withUser("kanan").password(password).roles("admin"); } @Bean PasswordEncoder password(){ return new BCryptPasswordEncoder(); } }
@Configuration public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(password()); } @Bean PasswordEncoder password(){ return new BCryptPasswordEncoder(); } }
@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
return new User("kanan",
new BCryptPasswordEncoder().encode("123456"),auths);
}
}
整合 MyBatis Plus 完成数据库操作
<!--mybatis plus--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.2</version> </dependency> <!--mysql--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for users -- ---------------------------- DROP TABLE IF EXISTS `users`; CREATE TABLE `users` ( `id` int NOT NULL AUTO_INCREMENT COMMENT '用户ID', `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户名', `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户密码', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of users -- ---------------------------- INSERT INTO `users` VALUES (1, 'kanan', '123456'); INSERT INTO `users` VALUES (2, 'wanan', '654321'); SET FOREIGN_KEY_CHECKS = 1;
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql:///security_test?serverTimezone=UTC&rewriteBatchedStatements=true
username: root
password: 123456
@Data @TableName("users") public class User { /** * 用户ID */ @TableId(type = IdType.AUTO) //自增策略 private Integer id; /** * 用户名 */ private String username; /** * 用户密码 */ private String password; }
@Mapper //因为启动类已经添加注解,所以这里可以不加mapper注解,主要防止主动注入时爆红
public interface UserMapper extends BaseMapper<User> {
}
@Service("userDetailsService") public class MyUserDetailsService implements UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //先通过username 查询数据库是否有这个用户 QueryWrapper<User> userQueryWrapper = new QueryWrapper<>(); userQueryWrapper.eq("username",username); User user = this.userMapper.selectOne(userQueryWrapper); if (user == null){//数据库没有当前用户名,认证失败 throw new UsernameNotFoundException("用户名不存在"); } //权限设置 List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role"); //从查询数据库返回的user对象,得到用户名和密码 return new User(user.getUsername(), new BCryptPasswordEncoder().encode(user.getPassword()),auths); } }
@Configuration public class SecurityConfiguration extends WebSecurityConfigurerAdapter { /** * 自定义登录页面设置 * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() //定义自己编写的登录页面 .loginPage("/login.html") //登录页面设置 .loginProcessingUrl("/user/login") //登录访问路径 .defaultSuccessUrl("/test/index").permitAll() //登录成功后跳转页面 .and().authorizeRequests() .antMatchers("/","/test/hello","/user/login").permitAll() //设置哪些路径不被保护 .anyRequest().authenticated() .and().csrf().disable(); //关闭csrf防护 } }
关于关闭csrf防护,在后面会慢慢说明
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <form action="/user/login" method="post"> <div> <lable>用户名:</lable> <input type="text" name="username"> </div> <div> <lable>密 码:</lable> <input type="password" name="password"> </div> <div> <input type="submit" value="登录"> </div> </form> </body> </html>
@RequestMapping("index")
public void index(){
return "Hello Index";
}
如果当前的主体具有指定的权限,有返回 true,没有则返回 false
//表示当前登录用户具有admins权限时才可以访问这个路径
.antMatchers("/test/index").hasAuthority("admins")
type=Forbidden:表示禁止访问,就是没有权限
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admins");
如果当前的主体具有在一些指定权限中是否有一个权限,有返回 true,没有则返回 false
.antMatchers("/test/index").hasAnyAuthority("admins","manage")
如果用户具备给定的角色就允许访问,否则出现403
如果当前主体具有指定的角色,则返回true
.antMatchers("/test/index").hasRole("sale")
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admins,ROLE_sale");
添加角色时用逗号隔开,注意角色需要添加 ROLE_ 前缀
private static String hasRole(String role) {
Assert.notNull(role, "role cannot be null"); //判断是否为null
//在设置访问角色时如果前缀是 ROLE_ 开始,则返回false,并提示,不要自己添加前缀,因为是它自己添加的(在下面retrun中可以看出结果)
Assert.isTrue(!role.startsWith("ROLE_"), () -> {
return "role should not start with 'ROLE_' since it is automatically inserted. Got '" + role + "'";
});
return "hasRole('ROLE_" + role + "')";
}
表示用户具备任何一个条件都可以访问
.antMatchers("/test/index").hasAnyRole("sale","develop")
自定义 403 没有权限访问的页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>没有访问权限</h1>
</body>
</html>
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
/**
* 自定义登录页面设置
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling().accessDeniedPage("/unAuthority.html");
}
}
判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀 ROLE_
@EnableGlobalMethodSecurity(securedEnabled = true)
@SpringBootApplication
public class SecurityDemo1Application {
public static void main(String[] args) {
SpringApplication.run(SecurityDemo1Application.class, args);
}
}
@Secured({"ROLE_sale","ROLE_manage"})
@GetMapping("delete")
public String delete(){
return "Hello Delete";
}
注解适合进入方法前的权限验证,可以将登录用户的 roles/permissions 参数传到方法中
@PreAuthorize("hasAnyRole('admins')")
@GetMapping("delete")
public String delete(){
return "Hello Delete";
}
表示方法执行后再进行校验
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
@PostAuthorize("hasAnyRole('admins')") //方法执行之后校验
@GetMapping("delete")
public String delete(){
System.out.println("Hello ROLE_admins");
return "Hello Delete";
}
此时如果用户不是这个角色,那么访问后会跳转到403界面,都是还是会执行方法中的内容
Hello ROLE_admins
权限验证之后对数据进行过滤
@PostFilter("filterObject.username == 'onana'")//只拿到 username为 onana 的数据,过滤掉其他数据
@PreAuthorize("hasAnyAuthority('admins')")
@GetMapping("getAll")
@ResponseBody
public List<Users> getAllUser(){
List<Users> users = new ArrayList<>();
users.add(new Users(null,"onana","123"));
users.add(new Users(null,"lsisi","123"));
return users;
}
[
{
"id": null,
"username": "onana",
"password": "123"
}
]
权限验证之前对数据进行过滤
http.logout()
.logoutUrl("/logout") //设置退出的请求地址
.logoutSuccessUrl("/test/hello").permitAll(); //退出完成后跳转页面
<a href="/logout">退出</a>
**实现原理:**首先用户登入成功后,会生成一个 token(加密串),这个 token,一边相应给浏览器,放到 cookie中,一边使用 token 和用户信息 存储到数据库中,而以后再次访问时,浏览器获取 cookie信息,拿着cookie信息到数据库进行比对,如果查询到对应信息,则认证成功,实现自动登录
DROP TABLE IF EXISTS `persistent_logins`;
CREATE TABLE `persistent_logins` (
`username` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`series` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`token` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`series`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
注入数据源,配置操作数据库对象
@Configuration public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; //注入数据源 @Autowired private DataSource dataSource; @Bean public PersistentTokenRepository persistentTokenRepository(){ JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); jdbcTokenRepository.setDataSource(dataSource); //自动创建表 //jdbcTokenRepository.setCreateTableOnStartup(true); return jdbcTokenRepository; } }
http.formLogin()
.and().rememberMe().tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(60) //设置有效时长(s)
.userDetailsService(userDetailsService)
<div>
<lable>记住我:</lable>
<input type="checkbox" name="remember-me"> <!--这里的name="remember-me" 是必须的 -->
</div>
当我们登入成功后,可以看到 security为我们自动封装的数据已经储存到数据库中了
csrf指的是:跨站请求伪造(Cross-site request forgery),也被称为one-click attack 或者 session riding 通常缩写为 CSRF 或者 XSRF,是一种挟制用户在当前已经登录的 Web 应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF利用的是网站对用户网页浏览器的信任。
跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了 web中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是不是用户自愿发出的
<!-- 使用模板引擎 -->
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
**实现原理:**在session中存放一个 token,保护 post,put,delete 请求,判断提交的请求中 token与session 中的是否一致,相同则放行
等待更新
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。