先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7
深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
如果你需要这些资料,可以添加V获取:vip1024b (备注Java)
正文
import lombok.Data;
import java.util.Set;
/**
-
@program: security-springmvc
-
@description:
-
@author: 步尔斯特
-
@create: 2021-07-22 23:25
*/
@Data
@AllArgsConstructor
public class UserDto {
public static final String SESSION_USER_KEY = “_user”;
//用户身份信息
private String id;
private String username;
private String password;
private String fullname;
private String mobile;
}
然后修改Logincontroller,认证成功后,将用户信息放入当前会话。并增加用户登出方法,登出时将session置为失效。
package com.uncle.security.springmvc.controller;
import com.uncle.security.springmvc.model.AuthenticationRequest;
import com.uncle.security.springmvc.model.UserDto;
import com.uncle.security.springmvc.service.AuthenticationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
/**
-
@program: security-springmvc
-
@description:
-
@author: 步尔斯特
-
@create: 2021-07-22 23:33
*/
@RestController
public class LoginController {
@Autowired
AuthenticationService authenticationService;
@RequestMapping(value = “/login”,produces = “text/plain;charset=utf-8”)
public String login(AuthenticationRequest authenticationRequest, HttpSession session){
UserDto userDto = authenticationService.authentication(authenticationRequest);
//存入session
session.setAttribute(UserDto.SESSION_USER_KEY,userDto);
return userDto.getUsername() +“登录成功”;
}
@GetMapping(value = “/logout”,produces = {“text/plain;charset=UTF-8”})
public String logout(HttpSession session){
session.invalidate();
return “退出成功”;
}
}
2、增加测试资源
修改Logincontroller ,增加测试资源1 ,它从当前会话session中获取当前登录用户,并返回提示信息给前台。
package com.uncle.security.springmvc.controller;
import com.uncle.security.springmvc.model.AuthenticationRequest;
import com.uncle.security.springmvc.model.UserDto;
import com.uncle.security.springmvc.service.AuthenticationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
/**
-
@program: security-springmvc
-
@description:
-
@author: 步尔斯特
-
@create: 2021-07-22 23:33
*/
@RestController
public class LoginController {
@Autowired
AuthenticationService authenticationService;
@RequestMapping(value = “/login”,produces = “text/plain;charset=utf-8”)
public String login(AuthenticationRequest authenticationRequest, HttpSession session){
UserDto userDto = authenticationService.authentication(authenticationRequest);
//存入session
session.setAttribute(UserDto.SESSION_USER_KEY,userDto);
return userDto.getUsername() +“登录成功”;
}
@GetMapping(value = “/logout”,produces = {“text/plain;charset=UTF-8”})
public String logout(HttpSession session){
session.invalidate();
return “退出成功”;
}
@GetMapping(value = “/r/r1”,produces = {“text/plain;charset=UTF-8”})
public String r1(HttpSession session){
String fullname = null;
Object object = session.getAttribute(UserDto.SESSION_USER_KEY);
if(object == null){
fullname = “匿名”;
}else{
UserDto userDto = (UserDto) object;
fullname = userDto.getFullname();
}
return fullname+“访问资源r1”;
}
}
3、测试
未登录情况下直接访问测试资源/r/r1 :
成功登录的情况下访问测试资源/r/r1 :
退出,访问地址/logout
测试结果说明,在用户登录成功时,该用户信息已被成功放入session ,并且后续请求可以正常从session中获取当前登录用户信息,退出时,session失效,再次访问为匿名访问,符合预期结果。
2.5 实现授权功能
现在我们已经完成了用户身份凭证的校验以及登录的状态保持,并且我们也知道了如何获取当前登录用户(从Session中获取)的信息,接下来,用户访问系统需要经过授权,即需要完成如下功能:
- 匿名用户(未登录用户)访问拦截:禁止匿名用户访问某些资源。
- 登录用户访问拦截:根据用户的权限决定是否能访问某些资源。
1、增加权限数据
为了实现这样的功能,我们需要在UserDto里增加权限属性,用于表示该登录用户所拥有的权限,同时修改 UserDto的构造方法。
package com.uncle.security.springmvc.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.Set;
/**
-
@program: security-springmvc
-
@description:
-
@author: 步尔斯特
-
@create: 2021-07-22 23:25
*/
@Data
@AllArgsConstructor
public class UserDto {
public static final String SESSION_USER_KEY = “_user”;
//用户身份信息
private String id;
private String username;
private String password;
private String fullname;
private String mobile;
/**
- 用户权限
*/
private Set authorities;
}
并在AuthenticationServicelmpI中为模拟用户初始化权限,其中张三给了p1权限,李四给了p2权限。
package com.uncle.security.springmvc.service;
import com.uncle.security.springmvc.model.AuthenticationRequest;
import com.uncle.security.springmvc.model.UserDto;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
-
@program: security-springmvc
-
@description:
-
@author: 步尔斯特
-
@create: 2021-07-22 23:27
*/
@Service
public class AuthenticationServiceImpl implements AuthenticationService{
/**
-
用户认证,校验用户身份信息是否合法
-
@param authenticationRequest 用户认证请求,账号和密码
-
@return 认证成功的用户信息
*/
@Override
public UserDto authentication(AuthenticationRequest authenticationRequest) {
//校验参数是否为空
if(authenticationRequest == null
|| StringUtils.isEmpty(authenticationRequest.getUsername())
|| StringUtils.isEmpty(authenticationRequest.getPassword())){
throw new RuntimeException(“账号和密码为空”);
}
//根据账号去查询数据库,这里测试程序采用模拟方法
UserDto user = getUserDto(authenticationRequest.getUsername());
//判断用户是否为空
if(user == null){
throw new RuntimeException(“查询不到该用户”);
}
//校验密码
if(!authenticationRequest.getPassword().equals(user.getPassword())){
throw new RuntimeException(“账号或密码错误”);
}
//认证通过,返回用户身份信息
return user;
}
//根据账号查询用户信息
private UserDto getUserDto(String userName){
return userMap.get(userName);
}
//用户信息
private Map<String,UserDto> userMap = new HashMap<>();
{
Set authorities1 = new HashSet<>();
authorities1.add(“p1”);//这个p1我们人为让它和/r/r1对应
Set authorities2 = new HashSet<>();
authorities2.add(“p2”);//这个p2我们人为让它和/r/r2对应
userMap.put(“zhangsan”,new UserDto(“1010”,“zhangsan”,“123”,“张三”,“133443”,authorities1));
userMap.put(“lisi”,new UserDto(“1011”,“lisi”,“456”,“李四”,“144553”,authorities2));
}
}
2、增加测试资源
我们想实现针对不同的用户能访问不同的资源,前提是得有多个资源,因此在Logincontroller中增加测试资源2。
package com.uncle.security.springmvc.controller;
import com.uncle.security.springmvc.model.AuthenticationRequest;
import com.uncle.security.springmvc.model.UserDto;
import com.uncle.security.springmvc.service.AuthenticationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
/**
-
@program: security-springmvc
-
@description:
-
@author: 步尔斯特
-
@create: 2021-07-22 23:33
*/
@RestController
public class LoginController {
@Autowired
AuthenticationService authenticationService;
@RequestMapping(value = “/login”,produces = “text/plain;charset=utf-8”)
public String login(AuthenticationRequest authenticationRequest, HttpSession session){
UserDto userDto = authenticationService.authentication(authenticationRequest);
//存入session
session.setAttribute(UserDto.SESSION_USER_KEY,userDto);
return userDto.getUsername() +“登录成功”;
}
@GetMapping(value = “/logout”,produces = {“text/plain;charset=UTF-8”})
public String logout(HttpSession session){
session.invalidate();
return “退出成功”;
}
@GetMapping(value = “/r/r1”,produces = {“text/plain;charset=UTF-8”})
public String r1(HttpSession session){
String fullname = null;
Object object = session.getAttribute(UserDto.SESSION_USER_KEY);
if(object == null){
fullname = “匿名”;
}else{
UserDto userDto = (UserDto) object;
fullname = userDto.getFullname();
}
return fullname+“访问资源r1”;
}
@GetMapping(value = “/r/r2”,produces = {“text/plain;charset=UTF-8”})
public String r2(HttpSession session){
String fullname = null;
Object userObj = session.getAttribute(UserDto.SESSION_USER_KEY);
if(userObj != null){
fullname = ((UserDto)userObj).getFullname();
}else{
fullname = “匿名”;
}
return fullname + " 访问资源2";
}
}
3、实现授权拦截器
在interceptor包下定义SimpleAuthenticationlnterceptor拦截器,实现授权拦截:
- 校验用户是否登录
- 校验用户是否拥有操作权限
package com.uncle.security.springmvc.interceptor;
import com.uncle.security.springmvc.model.UserDto;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
-
@program: security-springmvc
-
@description:
-
@author: 步尔斯特
-
@create: 2021-07-22 23:53
*/
@Component
public class SimpleAuthenticationInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//在这个方法中校验用户请求的url是否在用户的权限范围内
//取出用户身份信息
Object object = request.getSession().getAttribute(UserDto.SESSION_USER_KEY);
if(object == null){
//没有认证,提示登录
writeContent(response,“请登录”);
}
UserDto userDto = (UserDto) object;
//请求的url
String requestURI = request.getRequestURI();
if( userDto.getAuthorities().contains(“p1”) && requestURI.contains(“/r/r1”)){
return true;
}
if( userDto.getAuthorities().contains(“p2”) && requestURI.contains(“/r/r2”)){
return true;
}
writeContent(response,“没有权限,拒绝访问”);
return false;
}
//响应信息给客户端
private void writeContent(HttpServletResponse response, String msg) throws IOException {
response.setContentType(“text/html;charset=utf-8”);
PrintWriter writer = response.getWriter();
writer.print(msg);
writer.close();
}
}
在WebConfig中配置拦截器,匹配/r/**的资源为受保护的系统资源,访问该资源的请求进入SimpleAuthenticationInterceptor 拦截器。
package com.uncle.security.springmvc.config;
import com.uncle.security.springmvc.interceptor.SimpleAuthenticationInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
/**
-
@program: security-springmvc
-
@description:
-
@author: 步尔斯特
-
@create: 2021-07-22 21:34
*/
@Configuration//就相当于springmvc.xml文件
@EnableWebMvc
@ComponentScan(basePackages = “com.uncle.security.springmvc”
,includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})
public class WebConfig implements WebMvcConfigurer {
@Autowired
SimpleAuthenticationInterceptor simpleAuthenticationInterceptor;
//视频解析器
@Bean
public InternalResourceViewResolver viewResolver(){
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix(“/WEB-INF/view/”);
viewResolver.setSuffix(“.jsp”);
return viewResolver;
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController(“/”).setViewName(“login”);
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(simpleAuthenticationInterceptor).addPathPatterns(“/r/**”);
}
}
4、测试
未登录情况下,/r/r1与/r/r2均提示"请先登录
张三登录情况下,由于张三有p1权限,因此可以访问/r/r1 ,张三没有p2权限,访问/r/r2时提示"权限不足"。
同理,李四登录情况下,由于李四有p2权限,因此可以访问/r/r2,李四没有p1权限,访问/r/r1时提示"权限不足"。(图略过)
测试结果全部符合预期结果。
2.6 小结
基于Session的认证方式是一种常见的认证方式,至今还有非常多的系统在使用。我们在此小节使用Springmvc技术对它进行简单实现,旨在让大家更清晰实在的了解用户认证、授权以及会话的功能意义及实现套路,也就是它们分别干了哪些事儿?大概需要怎么做?而在正式生产项目中,我们往往会考虑使用第三方安全框架(如spring security ,shiro等安全框架)来实现认证授权功能,因为这样做能一定程度提高生产力,提高软件标准化程度,另外往往这些框架的可扩展性考虑的非常全面。但是缺点也非常明显,这些通用化组件为了提高支持范围会增加很多可能我们不需要的功能,结构上也会比较抽象,如果我们不够了解它,一旦出现问题,将会很难定位。
3.1 简介
SpringSecurity是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。由于它是Spring生态系统中的一员,因此它伴随着整个Spring生态系统不断修正、升级,在springboot项目中加入Spring Security更是十分简单,使用SpringSecurity减少了为企业系统安全控制编写大量重复代码的工作。
Spring Security的前身是Acegi Security,在被收纳为Spring子项目后正式更名为SpringSecurity。笔者在完成这篇文章时,SpringSecurity已经升级到了5.5.1版本(5.3.10.RELEASE),SpringSecurity5.x以后不仅增加了原生的OAuth框架,还支持更加现代化的密码加密方式,可以预见,在java应用安全领域,SpringSecurity会成为首先被推崇的安全解决方案。
3.2 创建工程
3.2.1 创建maven工程
创建maven工程 ,工程结构如下:
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd”>
4.0.0
com.uncle
spring-security-springmvc
1.0-SNAPSHOT
war
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
org.springframework.security
spring-security-web
5.1.4.RELEASE
org.springframework.security
spring-security-config
5.1.4.RELEASE
org.springframework
spring-webmvc
5.1.5.RELEASE
javax.servlet
javax.servlet-api
3.0.1
provided
org.projectlombok
lombok
1.18.8
security-springmvc
org.apache.tomcat.maven
tomcat7-maven-plugin
2.2
org.apache.maven.plugins
maven-compiler-plugin
1.81.8
maven-resources-plugin
utf-8
true
src/main/resources
true
**/*
src/main/java
**/*.xml
3.2.2 Spring容器配置
package com.uncle.security.springmvc.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
/**
-
@program: security-springmvc
-
@description:
-
@author: 步尔斯特
-
@create: 2021-07-22 21:26
*/
@Configuration
@ComponentScan(basePackages = “com.uncle.security.springmvc”
,excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class)})
public class ApplicationConfig {
}
3.2.4 Servlet Context配置
package com.uncle.security.springmvc.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
/**
-
@program: security-springmvc
-
@description:
-
@author: 步尔斯特
-
@create: 2021-07-22 21:34
*/
@Configuration//就相当于springmvc.xml文件
@EnableWebMvc
@ComponentScan(basePackages = “com.uncle.security.springmvc”
,includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})
public class WebConfig implements WebMvcConfigurer {
//视频解析器
@Bean
public InternalResourceViewResolver viewResolver(){
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix(“/WEB-INF/view/”);
viewResolver.setSuffix(“.jsp”);
return viewResolver;
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController(“/”).setViewName(“redirect:/login”);
}
}
此处插一句题外话,有人问我这个配置什么意思
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController(“/”).setViewName(“redirect:/login”);
}
这个配置确实不常见,但是确实有很大用处,其实理解起来也很容易,上面的写法等同于如下的写法
@Controller
public class TestContrller {
@RequestMapping(“/test”)
public String test() {
return “redirect:/login”;
}
}
这时可能会有同学问,如果当redirect后的字符串同时存在于url和页面会重定向哪里,经笔者验证后,答案是页面的优先级会更高,感兴趣的读者可以自行去验证,笔者就不再赘述。
3.2.4 加载Spring容器
在init包下定义Spring容器初始化类SpringApplicationlnitializer,此类实现WebApplicationlnitializer接口, Spring容器启动时加载WebApplicationlnitializer接口的所有实现类。
package com.uncle.security.springmvc.init;
import com.uncle.security.springmvc.config.ApplicationConfig;
import com.uncle.security.springmvc.config.WebConfig;
import com.uncle.security.springmvc.config.WebSecurityConfig;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
/**
-
@program: security-springmvc
-
@description:
-
@author: 步尔斯特
-
@create: 2021-07-22 21:47
*/
public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
//spring容器,相当于加载 applicationContext.xml
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{ApplicationConfig.class, WebSecurityConfig.class};
}
//servletContext,相当于加载springmvc.xml
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{WebConfig.class};
}
//url-mapping
@Override
protected String[] getServletMappings() {
return new String[]{“/”};
}
}
3.3 认证
3.3.1 认证页面
SpringSecurity默认提供认证页面。
3.3.2 安全配置
spring security提供了用户名密码登录、退出、会话管理等认证功能,只需要配置即可使用。
在config包下定义WebSecurityConfig ,安全配置的内容包括:用户信息、密码编码器、安全拦截机制。
package com.uncle.security.springmvc.config;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
/**
-
@program: spring-security-springmvc
-
@description:
-
@author: 步尔斯特
-
@create: 2021-07-23 00:41
*/
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//定义用户信息服务(查询用户信息)
@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername(“zhangsan”).password(“123”).authorities(“p1”).build());
manager.createUser(User.withUsername(“lisi”).password(“456”).authorities(“p2”).build());
return manager;
}
//密码编码器
@Bean
public PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
//安全拦截机制(最重要)
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers(“/r/**”).authenticated()//所有/r/**的请求必须认证通过
.anyRequest().permitAll()//除了/r/**,其它的请求可以访问
.and()
.formLogin()//允许表单登录
.successForwardUrl(“/login-success”);//自定义登录成功的页面地址
}
}
在userDetailsService()方法中,我们返回了一个UserDetailsService给spring容器,Spring
Security会使用它来获取用户信息。我们暂时使用InMemoryUserDetailsManager实现类,并在其中分别创建了zhangsan、lisi两个用户,并设置密码和权限。
而在configure()中,我们通过HttpSecurity设置了安全拦截规则,其中包含了以下内容:
- url匹配"/**的资源,经过认证后才能访问。
- 其他url完全开放。
- 支持form表单认证,认证成功后转向/login-success。
加载 WebSecurityConfig
修改SpringApplicationlinitializer的getRootConfigClasses()方法,添加WebSecurityConfig.class:
package com.uncle.security.springmvc.init;
import com.uncle.security.springmvc.config.ApplicationConfig;
import com.uncle.security.springmvc.config.WebConfig;
import com.uncle.security.springmvc.config.WebSecurityConfig;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
/**
-
@program: security-springmvc
-
@description:
-
@author: 步尔斯特
-
@create: 2021-07-22 21:47
*/
public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
//spring容器,相当于加载 applicationContext.xml
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{ApplicationConfig.class, WebSecurityConfig.class};
}
//servletContext,相当于加载springmvc.xml
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{WebConfig.class};
}
//url-mapping
@Override
protected String[] getServletMappings() {
return new String[]{“/”};
}
}
3.3.3 初始化
Spring Security初始化,这里有两种情况
- 若当前环境没有使用Spring或Spring MVC ,则需要将WebSecurityConfig(Spring Security配置类)传入超类,以确保获取配置,并创建spring context。
- 相反,若当前环境已经使用spring ,我们应该在现有的springContext中注册Spring Security(上一步已经将 WebSecurityConfig加载至rootcontext),此方法可以什么都不做。
package com.uncle.security.springmvc.init;
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
/**
-
@program: spring-security-springmvc
-
@description:
-
@author: 步尔斯特
-
@create: 2021-07-23 00:46
*/
@EnableWebSecurity
public class SpringSecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
public SpringSecurityApplicationInitializer() {
//super(WebSecurityConfig.class);
}
}
3.3.4 默认根路径请求
在WebConfig.java中添加默认请求根路径跳转到/login ,此url为spring security提供:
package com.uncle.security.springmvc.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
/**
-
@program: security-springmvc
-
@description:
-
@author: 步尔斯特
-
@create: 2021-07-22 21:34
*/
@Configuration//就相当于springmvc.xml文件
@EnableWebMvc
@ComponentScan(basePackages = “com.uncle.security.springmvc”
,includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})
public class WebConfig implements WebMvcConfigurer {
//视频解析器
@Bean
public InternalResourceViewResolver viewResolver(){
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix(“/WEB-INF/view/”);
viewResolver.setSuffix(“.jsp”);
return viewResolver;
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController(“/”).setViewName(“redirect:/login”);
}
}
spring security默认提供的登录页面。
3.3.5 认证成功页面
在安全配置中,认证成功将跳转到/login-success ,代码如下:
package com.uncle.security.springmvc.config;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
/**
-
@program: spring-security-springmvc
-
@description:
-
@author: 步尔斯特
-
@create: 2021-07-23 00:41
*/
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//定义用户信息服务(查询用户信息)
@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername(“zhangsan”).password(“123”).authorities(“p1”).build());
manager.createUser(User.withUsername(“lisi”).password(“456”).authorities(“p2”).build());
return manager;
}
//密码编码器
@Bean
public PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
//安全拦截机制(最重要)
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers(“/r/**”).authenticated()//所有/r/**的请求必须认证通过
.anyRequest().permitAll()//除了/r/**,其它的请求可以访问
.and()
.formLogin()//允许表单登录
.successForwardUrl(“/login-success”);//自定义登录成功的页面地址
}
}
spring security支持form表单认证,认证成功后转向/login-success。
在 Logincontroller 中定义/login-success:
package com.uncle.security.springmvc.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
/**
-
@program: security-springmvc
-
@description:
-
@author: 步尔斯特
-
@create: 2021-07-22 23:33
*/
@RestController
public class LoginController {
@RequestMapping(value = “/login-success”,produces = {“text/plain;charset=UTF-8”})
public String loginSuccess(){
return " 登录成功";
}
}
3.3.6 测试
启动项目,访问http://localhost:8080/spring-security-springmvc路径地址
页面会根据WebConfig中addViewControllers配置规则,跳转至/login , /login是Spring
Security提供的登录页面。
登录
输入错误的用户名、密码
输入正确的用户名、密码,登录成功
退出
请求/logout退出
退出后再访问资源自动跳转到登录页面
3.4 授权
实现授权需要对用户的访问进行拦截校验,校验用户的权限是否可以操作指定的资源,Spring Security默认提供授权实现方法。
在LoginController 添加/r/r1 或/r/r2
package com.uncle.security.springmvc.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
/**
-
@program: security-springmvc
-
@description:
-
@author: 步尔斯特
-
@create: 2021-07-22 23:33
*/
@RestController
public class LoginController {
@RequestMapping(value = “/login-success”,produces = {“text/plain;charset=UTF-8”})
public String loginSuccess(){
return " 登录成功";
}
/**
-
测试资源1
-
@return
*/
@GetMapping(value = “/r/r1”,produces = {“text/plain;charset=UTF-8”})
public String r1(){
return " 访问资源1";
}
/**
-
测试资源2
-
@return
*/
@GetMapping(value = “/r/r2”,produces = {“text/plain;charset=UTF-8”})
public String r2(){
return " 访问资源2";
}
}
在安全配置类WebSecurityConfig.java中配置授权规则:
package com.uncle.security.springmvc.config;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
/**
-
@program: spring-security-springmvc
-
@description:
-
@author: 步尔斯特
-
@create: 2021-07-23 00:41
*/
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//定义用户信息服务(查询用户信息)
@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername(“zhangsan”).password(“123”).authorities(“p1”).build());
manager.createUser(User.withUsername(“lisi”).password(“456”).authorities(“p2”).build());
return manager;
}
//密码编码器
@Bean
public PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
//安全拦截机制(最重要)
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers(“/r/r1”).hasAuthority(“p1”)
.antMatchers(“/r/r2”).hasAuthority(“p2”)
.antMatchers(“/r/**”).authenticated()//所有/r/**的请求必须认证通过
.anyRequest().permitAll()//除了/r/**,其它的请求可以访问
.and()
.formLogin()//允许表单登录
.successForwardUrl(“/login-success”);//自定义登录成功的页面地址
}
}
测试
登录成功
访问/r/r1和/r/r2 ,有权限时则正常访问,否则返回403 (拒绝访问)
4.1 集成 SpringBoot
4.1.1 Spring Boot 简介
Spring Boot是一套Spring的快速开发框架,基于Spring 4.0设计,使用Spring Boot开发可以避免一些繁琐的工程配置,同时它集成了大量的常用框架,快速导入依赖包,避免依赖包的冲突。基本上常用的开发框架都支持 SpringBoot开发,例如:MyBatis、Dubbo等,Spring 家族更是如此,例如:Spring Cloud、Spring mvc、Spring Security等,使用Spring Boot开发可以大大得高生产率,所以Spring Boo的使用率非常高。
本节讲解如何通过Spring Boot开发Spring Security应用,SpringBoot提供spring-boot-starter-security用于开发Spring Security应用。
4.1.2 创建maven工程
创建maven工程结构如下:
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd”>
4.0.0
com.uncle
spring-boot-security
1.0-SNAPSHOT
org.springframework.boot
spring-boot-starter-parent
2.1.3.RELEASE
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-security
javax.servlet
javax.servlet-api
provided
javax.servlet
jstl
org.springframework.boot
spring-boot-starter-tomcat
provided
org.apache.tomcat.embed
tomcat-embed-jasper
provided
org.projectlombok
lombok
1.18.0
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-starter-jdbc
mysql
mysql-connector-java
5.1.47
security-springboot
org.apache.tomcat.maven
tomcat7-maven-plugin
2.2
org.apache.maven.plugins
maven-compiler-plugin
1.81.8
maven-resources-plugin
utf-8
true
src/main/resources
true
**/*
src/main/java
**/*.xml
4.1.3 spring容器配置
SpringBoot工程启动会自动扫描启动类所在包下的所有Bean,加载到spring容器。
Spring Boot配置文件
在resources下添加application.yml,内容如下:
server:
#端口
port: 8080
#应用的上下文路径,也可以称为项目路径,是构成url地址的一部分
servlet:
context-path: /spring-boot-security
#项目名
spring:
application:
name: spring-boot-security
#默认的配置为/templates/和.html
#这里笔者就不用jsp了,前面用jsp旨在让读者理解配置前缀和后缀
#spring.mvc.view.prefix=/WEB-INF/view/
#spring.mvc.view.suffix=.jsp
Spring Boot 启动类
package com.uncle.seciruty.springboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
-
@program: spring-boot-security
-
@description:
-
@author: 步尔斯特
-
@create: 2021-07-23 19:35
*/
@SpringBootApplication
public class SecuritySpringBootApp {
public static void main(String[] args) {
SpringApplication.run(SecuritySpringBootApp.class,args);
}
}
4.1.4 Servlet Context配置
由于Spring boot starter自动装配机制,这里无需使用@EnableWebMvc与@ComponentScan
WebConfig如下
package com.uncle.seciruty.springboot.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
-
@program: spring-boot-security
-
@description:
-
@author: 步尔斯特
-
@create: 2021-07-23 19:38
*/
@Configuration//就相当于springmvc.xml文件
public class WebConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController(“/”).setViewName(“redirect:/login”);
}
}
关于视图解析器
#默认的配置为/templates/和.html
#这里笔者就不用jsp了,前面用jsp旨在让读者理解视图解析器的配置
#spring.mvc.view.prefix=/WEB-INF/view/
#spring.mvc.view.suffix=.jsp
4.1.5 安全配置
由于Spring boot starter自动装配机制,这里无需使用@EnableWebSecurity
WebSecurityConfig内容如下
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//定义用户信息服务(查询用户信息)
@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername(“zhangsan”).password(“123”).authorities(“p1”).build());
manager.createUser(User.withUsername(“lisi”).password(“456”).authorities(“p2”).build());
return manager;
}
//密码编码器
@Bean
public PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
//安全拦截机制(最重要)
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers(“/r/r1”).hasAuthority(“p1”)
.antMatchers(“/r/r2”).hasAuthority(“p2”)
.antMatchers(“/r/**”).authenticated()//所有/r/**的请求必须认证通过
.anyRequest().permitAll()//除了/r/**,其它的请求可以访问
.and()
.formLogin()//允许表单登录
.successForwardUrl(“/login-success”);//自定义登录成功的页面地址
}
}
4.1.6 测试
Logincontroller的内容
package com.uncle.seciruty.springboot.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
-
@program: spring-boot-security
-
@description:
-
@author: 步尔斯特
-
@create: 2021-07-23 19:41
*/
@RestController
public class LoginController {
@RequestMapping(value = “/login-success”,produces = {“text/plain;charset=UTF-8”})
public String loginSuccess(){
//提示具体用户名称登录成功
return getUsername()+" 登录成功";
}
/**
-
测试资源1
-
@return
*/
@GetMapping(value = “/r/r1”,produces = {“text/plain;charset=UTF-8”})
public String r1(){
return getUsername()+" 访问资源1";
}
/**
-
测试资源2
-
@return
*/
@GetMapping(value = “/r/r2”,produces = {“text/plain;charset=UTF-8”})
public String r2(){
return getUsername()+" 访问资源2";
}
}
测试过程
不出意外的话,此时应该是报错了
原因:
这是因为添加了数据库组件,所以autoconfig会去读取数据源配置,而新建的项目还没有配置数据源URL地址错误,所以会导致异常出现。
解决方案:
在启动类的@EnableAutoConfiguration或@SpringBootApplication中添加exclude ={DataSourceAutoConfiguration.class},排除此类的autoconfig,启动以后就可以正常运行。
package com.uncle.seciruty.springboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
/**
-
@program: spring-boot-security
-
@description:
-
@author: 步尔斯特
-
@create: 2021-07-23 19:35
*/
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class SecuritySpringBootApp {
public static void main(String[] args) {
SpringApplication.run(SecuritySpringBootApp.class,args);
}
}
接下来,我们开始正式测试
1、测试认证
2、测试退出
3、测试授权
4.2 工作原理
4.2.1 结构总览
Spring Security所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入系统的请求进行拦截,校验每个请求是否能够访问它所期望的资源。根据前边知识的学习,可以通过Filter或AOP等技术来实现,Spring Security对Web资源的保护是靠Filter实现的,所以从这个Filter来入手,逐步深入Spring Security原理。
当初始化Spring Security时,会创建一个名为SpringSecurityFilterChain的Servlet过滤器,类型为org.springframework.security.web.FilterchainProxy ,它实现了javax.servlet.Filter,因此外部的请求会经过此类,下图是Spring Security过滤器链结构图:
FilterChainProxy是一个代理,真正起作用的是FilterChainProxy中SecurityFilterChain所包含的各个Filter,同时这些Filter作为Bean被Spring管理,它们是Spring Security核心,各有各的职责,但他们并不直接处理用户的认证,也不直接处理用户的授权,而是把它们交给了认证管理器(AuthenticationManager)和决策管理器(AccessDecisionManager)进行处理,下图是FilterChainProxy相关类的关系图:
spring Security功能的实现主要是由一系列过滤器链相互配合完成
下面介绍过滤器链中主要的几个过滤器及其作用:
SecurityContextPersistenceFilter
SecurityContextPersistenceFilter这个Filter是整个拦截过程的入口和出口(也就是第一个和最后一个拦截器),会在请求开始时从配置好的SecurityContextRepository中获取SecurityContext,然后把它设置给SecurityContextHolder,在请求完成后将SecurityContextHolder持有的Securitycontext再保存到配置好的 SecurityContextRepository ,同时清除SecurityContextHolder 所持有的SecurityContext
UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationFilter用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,其内部还有登录成功或失败后进行处理的AuthenticationSuccessHandler和
AuthenticationFailureHandler,这些都可以根据需求做相关改变
Filtersecurityinterceptor
Filtersecurityinterceptor是用于保护web资源的,使用AccessDecisionManager对当前用户进行授权访问
ExceptionTranslationFilter
ExceptionTranslationFilter能够捕获来自FilterChain所有的异常,并进行处理。但是它只会处理两类异常:AuthenticationException和 AccessDeniedException ,其它的异常它会继续抛出。
4.2.2 认证流程
流程图
认证过程分析:
1.用户提交用户名 密码被 SecurityFilterChain 中的UsernamePasswordAuthenticationFilter 过滤器获取到, 封装为请求Authentication ,通常情况下是UsernamePasswordAuthenticationToken这个实现类。
2.然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证
3.认证成功后,AuthenticationManager身份管理器返回一个被填充满了信息的(包括上面提到的权限信息, 身份信息,细节信息,但密码通常会被移除)Authentication实例。
4.SecurityContextHolder安全上下文容器将第3步填充了信息的Authentication ,通过 SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。
可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口 ,也是发起认证的出发点,它的实现类为ProviderManager。而SpringSecurity支持多种认证方式,因此ProviderManager维护着一个List列表,存放多种认证方式,最终实际的认证工作是由AuthenticationProvider完成的。web表单的对应的AuthenticationProvider实现类为DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取。最终AuthenticationProvider将UserDetails填充至Authentication。
认证核心组件的大体关系如下:
4.2.2.1 AuthenticationProvider
通过前面的Spring Security认证流程我们得知,认证管理器(AuthenticationManager)委托 AuthenticationProvider 完成认证工作。
AuthenticationProvider是一个接口 ,定义如下:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.security.authentication;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
public interface AuthenticationProvider {
Authentication authenticate(Authentication var1) throws AuthenticationException;
boolean supports(Class<?> var1);
}
authenticate()方法定义了认证的实现过程,它的参数是一个Authentication ,里面包含了登录用户所提交的用户 密码等。而返回值也是一个Authentication ,这个Authentication则是在认证成功后,将用户的权限及其他信息重新组装后生成。
Spring Security中维护着一个List列表,存放多种认证方式,不同的认证方式使用不同的AuthenticationProvider。如使用用户名密码登录时,使用AuthenticationProviderl ,短信登录时使用 Authentication Provider2等等这样的例子很多。
每个AuthenticationProvider需要实现supports ()方法来表明自己支持的认证方式,如我们使用表单方式认证, 在提交请求时Spring Security会生成UsernamePasswordAuthenticationToken ,它是一个Authentication ,里面封装着用户提交的用户名、密码信息。
而对应哪个AuthenticationProvider来处理它?
我们在 DaoAuthenticationProvider的基类 AbstractUserDetailsAuthenticationProvider 发现以下代码:
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}//149行
也就是说当web表单提交用户名密码时,Spring Security由DaoAuthenticationProvider处理。
最后,我们来看一下Authentication(认证信息)的结构,它是一个接口 ,我们之前提到的 UsernamePasswordAuthenticationToken就是它的实现之一:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.security.core;
import java.io.Serializable;
import java.security.Principal;
import java.util.Collection;
public interface Authentication extends Principal, Serializable {//1
Collection<? extends GrantedAuthority> getAuthorities();//2
Object getCredentials();//3
Object getDetails();//4
Object getPrincipal();//5
boolean isAuthenticated();
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
(1 ) Authentication是spring security包中的接口,直接继承自Principal类,而Principal是位于 java.security 包中的。它是表示着一个抽象主体身份,任何主体都有一个名称,因此包含一个getName()方法。
(2 ) getAuthorities(),权限信息列表,默认是GrantedAuthority接口的一些实现类,通常是代表权限信息的一系列字符串。
(3 ) getCredentials(),凭证信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全。
(4 ) getDetails(),细节信息,web应用中的实现接口通常为WebAuthenticationDetails ,它记录了访问者的ip地址和sessionld的值。
(5 ) getPrincipal(),身份信息,大部分情况下返回的是UserDetails接口的实现类,UserDetails代表用户的详细信息,从Authentication中取出来的UserDetails就是当前登录用户信息,它也是框架中的常用接口之一。
附参考代码:
/*
-
Copyright © 1996, 2013, Oracle and/or its affiliates. All rights reserved.
-
ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*/
package java.security;
import javax.security.auth.Subject;
/**
-
This interface represents the abstract notion of a principal, which
-
can be used to represent any entity, such as an individual, a
-
corporation, and a login id.
-
@see java.security.cert.X509Certificate
-
@author Li Gong
*/
public interface Principal {
/**
-
Compares this principal to the specified object. Returns true
-
if the object passed in matches the principal represented by
-
the implementation of this interface.
-
@param another principal to compare with.
-
@return true if the principal passed in is the same as that
-
encapsulated by this principal, and false otherwise.
*/
public boolean equals(Object another);
/**
-
Returns a string representation of this principal.
-
@return a string representation of this principal.
*/
public String toString();
/**
-
Returns a hashcode for this principal.
-
@return a hashcode for this principal.
*/
public int hashCode();
/**
-
Returns the name of this principal.
-
@return the name of this principal.
*/
public String getName();
/**
-
Returns true if the specified subject is implied by this principal.
-
The default implementation of this method returns true if
-
{@code subject} is non-null and contains at least one principal that
-
is equal to this principal.
-
Subclasses may override this with a different implementation, if
-
necessary.
-
@param subject the {@code Subject}
-
@return true if {@code subject} is non-null and is
-
implied by this principal, or false otherwise.
-
@since 1.8
*/
public default boolean implies(Subject subject) {
if (subject == null)
return false;
return subject.getPrincipals().contains(this);
}
}
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.security.core;
import java.io.Serializable;
public interface GrantedAuthority extends Serializable {
String getAuthority();
}
4.2.2.2 UserDetailsService
DaoAuthenticationProvider处理了web表单的认证逻辑,认证成功后既得到一个Authentication(UsernamePasswordAuthenticationToken实现),里面包含了身份信息(Principal) 。 这个身份信息就是一个object ,大多数情况下它可以被强转为UserDetails对象。
DaoAuthenticationProvider中包含了一个UserDetailsService实例,它负责根据用户名提取用户信息UserDetails(包含密码),而后DaoAuthenticationProvider会去对比UserDetailsService提取的用户密码与用户提交的密码是否匹配作为认证成功的关键依据,因此可以通过将自定义的UserDetailsService公开为spring bean来自定义身份验证。
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.security.core.userdetails;
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
很多人把DaoAuthenticationProvider和UserDetailsService的职责搞混淆,其实UserDetailsService只负责从特定的地方(通常是数据库)加载用户信息,仅此而已。而DaoAuthenticationProvider的职责更大,它完成完整的认证流程,同时会把UserDetails填充至Authentication。
上面一直提到UserDetails是用户信息:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.security.core.userdetails;
import java.io.Serializable;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
它和 Authentication 接口很类似,比如它们都拥有 username ,authorities。Authentication 的getCredentials()与UserDetails中的getPassword()需要被区分对待,前者是用户提交的密码凭证,后者是用户实际存储的密码,认证其实就是对这两者的比对。Authentication中的getAuthorities()实际是由UserDetails的getAuthorities()传递而形成的。Authentication接口中的getDetails()方法,其中的UserDetails用户详细信息便是经过了 AuthenticationProvider认证之后被填充的。
通过实现UserDetailsService和UserDetails ,我们可以完成对用户信息获取方式以及用户信息字段的扩展。
Spring Security提供的lnMemoryllserDetailsManager(内存认证),JdbcUserDetailsManager(jdbc认证)就是 UserDetailsService的实现类,主要区别无非就是从内存还是从数据库加载用户。
测试
自定义UserDetailsService
package com.uncle.seciruty.springboot.service;
import org.springframework.beans.factory.annotation.Autowired;
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.Service;
import java.util.List;
/**
-
@program: spring-boot-security
-
@description:
-
@author: 步尔斯特
-
@create: 2021-08-06 22:05
*/
@Service
public class SpringDataUserDetailsService implements UserDetailsService {
@Autowired
UserDao userDao;
//根据 账号查询用户信息
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//将来连接数据库根据账号查询用户信息
UserDto userDto = userDao.getUserByUsername(username);
if(userDto == null){
//如果用户查不到,返回null,由provider来抛出异常
return null;
}
//根据用户的id查询用户的权限
List permissions = userDao.findPermissionsByUserId(userDto.getId());
//将permissions转成数组
String[] permissionArray = new String[permissions.size()];
permissions.toArray(permissionArray);
UserDetails userDetails = User.withUsername(userDto.getUsername()).password(userDto.getPassword()).authorities(permissionArray).build();
return userDetails;
}
}
屏蔽安全配置类中UserDetailsService的定义
package com.uncle.seciruty.springboot.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
/**
-
@program: spring-boot-security
-
@description:
-
@author: 步尔斯特
-
@create: 2021-07-23 19:40
*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//定义用户信息服务(查询用户信息)
// @Bean
// public UserDetailsService userDetailsService(){
// InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
// manager.createUser(User.withUsername(“zhangsan”).password(“123”).authorities(“p1”).build());
// manager.createUser(User.withUsername(“lisi”).password(“456”).authorities(“p2”).build());
// return manager;
// }
//密码编码器
@Bean
public PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
//安全拦截机制(最重要)
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers(“/r/r1”).hasAuthority(“p1”)
.antMatchers(“/r/r2”).hasAuthority(“p2”)
.antMatchers(“/r/**”).authenticated()//所有/r/**的请求必须认证通过
.anyRequest().permitAll()//除了/r/**,其它的请求可以访问
.and()
.formLogin()//允许表单登录
.successForwardUrl(“/login-success”);//自定义登录成功的页面地址
}
}
重启工程,请求认证,SpringDataUserDetailsService的loadUserByUsername方法被调用,查询用户信息。
4.2.2.3 PasswordEncoder
DaoAuthenticationProvide以证处理器通过UserDetailsService获取到UserDetails后,它是如何与请求 Authentication中的密码做对比呢?在这里Spring Security为了适应多种多样的加密类型,又做了抽象,DaoAuthenticationProvider通过 PasswordEncoder接口的matches方法进行密码的对比,而具体的密码对比细节取决于实现:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.security.crypto.password;
public interface PasswordEncoder {
String encode(CharSequence var1);
boolean matches(CharSequence var1, String var2);
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
而Spring Security提供很多内置的PasswordEncoder,能够开箱即用,使用某种PasswordEncoder只需要进行如下声明即可,如下:
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
NoOpPasswordEncoder采用字符串匹配方法,不对密码进行加密比较处理,密码比较流程如下:
1、用户输入密码(明文)
2、DaoAuthenticationProvider获取UserDetails (其中存储了用户的正确密码)
3、DaoAuthenticationProvider使用PasswordEncoder对输入的密码和正确的密码进行校验,密码一致则校验通过,否则校验失败。
NoOpPasswordEncode啲校验规则拿输入的密码和UserDetails中的正确密码进行字符串比较,字符串内容一致 则校验通过,否则校验失败。
实际项目中推荐使用BCryptPasswordEncoder, Pbkdf2PasswordEncoder,SCryptPasswordEncoder等。
使用BCryptPasswordEncoder
1、 配置BCryptPasswordEncoder 在安全配置类中定义:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
测试发现认证失败,提示:Encoded password does not look like BCrypt。
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException(“rawPassword cannot be null”);
} else if (encodedPassword != null && encodedPassword.length() != 0) {
if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
this.logger.warn(“Encoded password does not look like BCrypt”);
return false;
} else {
return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
}
} else {
this.logger.warn(“Empty encoded password”);
return false;
}
}
原因:
由于UserDetails中存储的是原始密码(比如:123 ),它不是BCrypt格式。 跟踪DaoAuthenticationProvider第33行代码查看userDetails中的内容,跟踪第38行代码查看 Password Encoder的类型。
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {//33行
this.logger.debug(“Failed to authenticate since no credentials provided”);
throw new BadCredentialsException(this.messages.getMessage(“AbstractUserDetailsAuthenticationProvider.badCredentials”, “Bad credentials”));
} else {
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {//38行
this.logger.debug(“Failed to authenticate since password does not match stored value”);
throw new BadCredentialsException(this.messages.getMessage(“AbstractUserDetailsAuthenticationProvider.badCredentials”, “Bad credentials”));
}
}
}
测试BCrypt
通过下边的代码测试BCrypt加密及校验的方法
添加依赖:
org.springframework.boot
spring-boot-starter-test
test
编写测试方法:
package com.uncle.seciruty.springboot.util;
import org.springframework.security.crypto.bcrypt.BCrypt;
/**
-
@program: spring-boot-security
-
@description:
-
@author: 步尔斯特
-
@create: 2021-08-06 22:12
*/
public class BCryptUtil {
public static void main(String[] args) {
String hashpw = BCrypt.hashpw(“456”, BCrypt.gensalt());
System.out.println(hashpw);
boolean checkpw = BCrypt.checkpw(“456”,“$2a 10 10 10bcJXXryMCxXtkxRkG1UekOkOe0BqxiqOYKJzGni64jnyWAD15wmDy”);
System.out.println(checkpw);
//123 -> $2a$10$8iHn2TEvyzkUgO2np9glzufe.wtRyjA5u3xfvs/D.9FCzm1XvCAGm
//456
}
}
修改安全配置类
将UserDetails中的原始密码修改为BCrypt格式:
//密码编码器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
实际项目中存储在数据库中的密码并不是原始密码,都是经过加密处理的密码。
4.2.3 授权流程
Spring Security可以通过http.authorizeRequests()对web请求进行授权保护。Spring Security使用标准Filter建立了对web请求的拦截,最终实现对资源的授权访问。
Spring Security的授权流程如下:
分析授权流程:
1.拦截请求,已认证用户访问受保护的web资源将被SecurityFilterChain中的FiltersecurityInterceptor的子类拦截。
2.获取资源访问策略,FilterSecurityInterceptor会从 SecurityMetadataSource 的子类 DefaultFilterlnvocationSecurityMetadataSource获取要访问当前资源所需要的权限Collection 。
SecurityMetadataSource其实就是读取访问策略的抽象,而读取的内容,其实就是我们配置的访问规则,读取访问策略如:
http
.authorizeRequests()
.antMatchers(“/r/r1”).hasAuthority(“p1”)
.antMatchers(“/r/r2”).hasAuthority( “p2”)
3.Filtersecurityinterceptor会调用AccessDecisionManager进行授权决策,若决策通过,则允许访问资源,否则将禁止访问。
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.security.access;
import java.util.Collection;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
public interface AccessDecisionManager {
/**
*通过传递的参数来决定用户是否有访问对应受保护资源的权限
*/
void decide(Authentication var1, Object var2, Collection var3) throws AccessDeniedException, InsufficientAuthenticationException;
boolean supports(ConfigAttribute var1);
boolean supports(Class<?> var1);
}
这里着重说明一下decide的参数:
authentication :要访问资源的访问者的身份
object :要访问的受保护资源,web请求对应Fi足revocation
configAttributes :是受保护资源的访问策略,通过SecurityMetadataSource获取。
decide接口就是用来鉴定当前用户是否有访问对应受保护资源的权限。
授权决策
AccessDecisionManager采用投票的方式来确定是否能够访问受保护资源。
AccessDecisionManager中包含的一系列AccessDecisionVoter将会被用来对Authentication是否有权访问受保护对象进行投票,AccessDecisionManager根据投票结果,做出最终决策。
AccessDecisionVoter是一个接口 ,其中定义有三个方法,具体结构如下所示。
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.security.access;
import java.util.Collection;
import org.springframework.security.core.Authentication;
public interface AccessDecisionVoter {
int ACCESS_GRANTED = 1;
int ACCESS_ABSTAIN = 0;
int ACCESS_DENIED = -1;
boolean supports(ConfigAttribute var1);
boolean supports(Class<?> var1);
int vote(Authentication var1, S var2, Collection var3);
}
vote()方法的返回结果会是AccessDecisionVoter中定义的三个常量之一。
- ACCESS_GRANTED表示同意
最后
作为过来人,小编是整理了很多进阶架构视频资料、面试文档以及PDF的学习资料,针对上面一套系统大纲小编也有对应的相关进阶架构视频资料
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
赖:
org.springframework.boot
spring-boot-starter-test
test
编写测试方法:
package com.uncle.seciruty.springboot.util;
import org.springframework.security.crypto.bcrypt.BCrypt;
/**
-
@program: spring-boot-security
-
@description:
-
@author: 步尔斯特
-
@create: 2021-08-06 22:12
*/
public class BCryptUtil {
public static void main(String[] args) {
String hashpw = BCrypt.hashpw(“456”, BCrypt.gensalt());
System.out.println(hashpw);
boolean checkpw = BCrypt.checkpw(“456”,“$2a 10 10 10bcJXXryMCxXtkxRkG1UekOkOe0BqxiqOYKJzGni64jnyWAD15wmDy”);
System.out.println(checkpw);
//123 -> $2a$10$8iHn2TEvyzkUgO2np9glzufe.wtRyjA5u3xfvs/D.9FCzm1XvCAGm
//456
}
}
修改安全配置类
将UserDetails中的原始密码修改为BCrypt格式:
//密码编码器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
实际项目中存储在数据库中的密码并不是原始密码,都是经过加密处理的密码。
4.2.3 授权流程
Spring Security可以通过http.authorizeRequests()对web请求进行授权保护。Spring Security使用标准Filter建立了对web请求的拦截,最终实现对资源的授权访问。
Spring Security的授权流程如下:
分析授权流程:
1.拦截请求,已认证用户访问受保护的web资源将被SecurityFilterChain中的FiltersecurityInterceptor的子类拦截。
2.获取资源访问策略,FilterSecurityInterceptor会从 SecurityMetadataSource 的子类 DefaultFilterlnvocationSecurityMetadataSource获取要访问当前资源所需要的权限Collection 。
SecurityMetadataSource其实就是读取访问策略的抽象,而读取的内容,其实就是我们配置的访问规则,读取访问策略如:
http
.authorizeRequests()
.antMatchers(“/r/r1”).hasAuthority(“p1”)
.antMatchers(“/r/r2”).hasAuthority( “p2”)
3.Filtersecurityinterceptor会调用AccessDecisionManager进行授权决策,若决策通过,则允许访问资源,否则将禁止访问。
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.security.access;
import java.util.Collection;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
public interface AccessDecisionManager {
/**
*通过传递的参数来决定用户是否有访问对应受保护资源的权限
*/
void decide(Authentication var1, Object var2, Collection var3) throws AccessDeniedException, InsufficientAuthenticationException;
boolean supports(ConfigAttribute var1);
boolean supports(Class<?> var1);
}
这里着重说明一下decide的参数:
authentication :要访问资源的访问者的身份
object :要访问的受保护资源,web请求对应Fi足revocation
configAttributes :是受保护资源的访问策略,通过SecurityMetadataSource获取。
decide接口就是用来鉴定当前用户是否有访问对应受保护资源的权限。
授权决策
AccessDecisionManager采用投票的方式来确定是否能够访问受保护资源。
AccessDecisionManager中包含的一系列AccessDecisionVoter将会被用来对Authentication是否有权访问受保护对象进行投票,AccessDecisionManager根据投票结果,做出最终决策。
AccessDecisionVoter是一个接口 ,其中定义有三个方法,具体结构如下所示。
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.security.access;
import java.util.Collection;
import org.springframework.security.core.Authentication;
public interface AccessDecisionVoter {
int ACCESS_GRANTED = 1;
int ACCESS_ABSTAIN = 0;
int ACCESS_DENIED = -1;
boolean supports(ConfigAttribute var1);
boolean supports(Class<?> var1);
int vote(Authentication var1, S var2, Collection var3);
}
vote()方法的返回结果会是AccessDecisionVoter中定义的三个常量之一。
- ACCESS_GRANTED表示同意
最后
作为过来人,小编是整理了很多进阶架构视频资料、面试文档以及PDF的学习资料,针对上面一套系统大纲小编也有对应的相关进阶架构视频资料
[外链图片转存中…(img-zxnPgEyp-1713571878844)]
[外链图片转存中…(img-iy1Nm5g8-1713571878845)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-PtpM8wwH-1713571878845)]
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!