认证和授权是SpringSecurity作为一个安全框架的核心功能。
快速入门
- 导入依赖
// spring security
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
// web
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
- 自定义前台登录页面login.html如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<form action="/login" method="post">
<!-- 用户名和密码必须是username和password-->
用户名:<input type="text" name="username"/><br/>
密码:<input type="password" name="password"/><br/>
<input type="submit" value="登录"/>
</form>
</body>
</html>
- 用户访问
http://localhost:8080/login.html
,将会跳转到spring security的内置登录页面,使用用户名user和Spring security生成的密码登录成功后才会跳转到我们自定义的登录页面。
实现自定义登录逻辑
- 几个重要的地方:UserDetailsService接口的loadUserByUsername方法和PasswordEncoder接口的BCryptPasswordEncoder实现类(官方推荐的一个密码解析器)
// BCryptPasswordEncoder示例 // 创建密码解析器 BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); // 加密密码 String encodedPassword = encoder.encode("123456"); System.out.println(encodedPassword); boolean matches = encoder.matches("123456", encodedPassword); System.out.println(matches); // true
- 实现自定义登录逻辑步骤
- 配置类
/** * Security配置类 */ @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { // 往spring容器中注入passwordEncoder bean @Bean public PasswordEncoder getPw() { return new BCryptPasswordEncoder(); } }
- 登录逻辑
@Service public class UserDetailServiceImpl implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 根据用户名去数据库查询 if (!"admin".equals(username)) { throw new UsernameNotFoundException("用户名不存在!"); } // 比较密码,匹配成功则返回UserDetails // password模拟用户注册时的密码,已经加密 String password = passwordEncoder.encode("123"); return new User(username, password, AuthorityUtils .createAuthorityList("admin,normal")); } }
- 用户输入
http://localhost:8080/login.html
后进入spring security内置登录页面,输入用户名admin,密码123登录成功。
自定义登录页面,失败跳转页面
- 自定义登录页面
- 前台登录页面如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <form action="/login" method="post"> <!-- 用户名和密码必须是username和password--> 用户名:<input type="text" name="username"/><br/> 密码:<input type="password" name="password"/><br/> <input type="submit" value="登录"/> </form> </body> </html>
- 配置类如下:
/** * Security配置类 */ @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { // 往spring容器中注入passwordEncoder bean @Bean public PasswordEncoder getPw() { return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { // 表单提交 http.formLogin() // 指定登录页面为自定义的 .loginPage("/login.html") // 自定义入参 .usernameParameter("username") .passwordParameter("password") // 登录页面表单提交地址,/login是一个控制器 .loginProcessingUrl("/login") // 登录成功后跳转的页面,/toMain也是一个控制器 // .successForwardUrl("/toMain") // 自定义登录成功处理器 .successHandler(new MyAuthenticationSuccessHandler("/main.html")) // 登录失败后跳转的页面,/toError也是一个控制器 // .failureForwardUrl("/toError"); // 自定义登录失败处理器 .failureHandler(new MyAuthenticationFailureHandler("/error.html")); // 授权 http.authorizeRequests() // 放行/error.html,不需要认证 .antMatchers("/error.html").permitAll() // 放行/login.html,不需要认证 .antMatchers("/login.html").permitAll() //.antMatchers("/css/**","/js/**","/images/**").permitAll() // 放行项目下所有以jpg结尾的图片 //.antMatchers("/**/*.jpg").permitAll() // 放行x.jpg // .regexMatchers() // 所有请求都必须认证才能访问,必须登录 .anyRequest().authenticated(); // 关闭csrf防护 http.csrf().disable(); } }
- 自定义登录失败跳转页面
- 登录失败页面error.html如下
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> 操作失败,请重新登录 <a href= "/login.html">跳转</a> </body> </html>
- 配置类如上
自定义登录成功处理器和登录失败处理器
- 自定义登录成功处理器,可以控制登录成功后做一些事情。
- 自定义登录成功处理器
// 自定义登录成功处理器,可以重定向(默认行为是请求转发) public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler { private String url; public MyAuthenticationSuccessHandler() { } public MyAuthenticationSuccessHandler(String url) { this.url = url; } @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { // 重定向 response.sendRedirect(url); // 获取用户名和密码,权限 User user = (User) authentication.getPrincipal(); System.out.println(user.getUsername()); System.out.println(user.getPassword()); // null System.out.println(user.getAuthorities()); } }
- 使用
// 表单提交 http.formLogin() // 自定义登录成功处理器,和successForwardUrl不能共存 .successHandler(new MyAuthenticationSuccessHandler("/main.html"))
- 自定义登录失败处理器
// 自定义登录失败处理器,可以重定向
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
private String url;
public MyAuthenticationFailureHandler() {
}
public MyAuthenticationFailureHandler(String url) {
this.url = url;
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.sendRedirect(url);
}
}
// 使用
// 表单提交
http.formLogin()
// 自定义登录失败处理器,和failureForwardUrl不能共存
.failureHandler(new MyAuthenticationFailureHandler("/error.html"));
访问控制URL匹配
- anyRequest():匹配所有请求
- antMatcher():匹配URL规则
- regexMatchers():使用正则表达式进行匹配
- mvcMatchers():适用于配置了servletPath的情况
- mvcMatchers():适用于配置了servletPath的情况
访问控制方法
- permitAll():表示匹配的URL任何人都允许访问
- authenticated():所匹配的 URL都需要被认证才能访问。
- denyAll()表示所匹配的 URL 都不允许被访问。
- rememberMe()被“remember me”的用户允许访问
// 授权
http.authorizeRequests()
// 放行/error.html,不需要认证
.antMatchers("/error.html").permitAll()
// 放行/login.html,不需要认证
.antMatchers("/login.html").permitAll()
//.antMatchers("/css/**","/js/**","/images/**").permitAll()
// 放行项目下所有以jpg结尾的图片
//.antMatchers("/**/*.jpg").permitAll()
// 放行x.jpg
// .regexMatchers()
// 指定请求方法为POST,放行所有x.jpg,不需要登录
.regexMatchers(HttpMethod.POST,".+[.]jpg").permitAll()
// 所有请求都必须认证才能访问,必须登录
.anyRequest().authenticated();
基于权限控制
// 权限控制,只有用于admin权限的用户才能访问xxx.html
.antMatchers("/xxx.html").hasAuthority("admin")
基于角色控制
// 权限和角色在自定义登录逻辑中定义
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 根据用户名去数据库查询,这里直接使用用户名模拟
if (!"admin".equals(username)) {
throw new UsernameNotFoundException("用户名不存在!");
}
// 比较密码,匹配成功则返回UserDetails
// password模拟用户注册时的密码,已经加密
String password = passwordEncoder.encode("123");
// ROLE_abc表示给用户赋予abc这个角色
return new User(username, password, AuthorityUtils
.createAuthorityList("admin,normal,ROLE_abc"));
}
}
// 角色控制
.antMatchers("/xxx.html").hasRole("abc")
基于IP控制
// 基于IP
.antMatchers("/xxx.html").hasIpAddress("127.0.0.1")
自定义403处理方案
- 实现AccessDeniedHandler接口
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setHeader("Content-Type", "application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("{\"status\":\"error\",\"msg\":\"权限不足,请联系管理员!\"}");
out.flush();
out.close();
}
}
- 配置类中添加异常处理器
// handler是在配置类中自动注入的
http.exceptionHandling().accessDeniedHandler(handler);
自定义访问控制逻辑
- 示例:判断登录的用户是否具有访问当前的URL权限
- 接口
public interface MyService { boolean hasPermission(HttpServletRequest request, Authentication authentication); }
- 实现类
@Service public class MyServiceImpl implements MyService { @Override public boolean hasPermission(HttpServletRequest request, Authentication authentication) { Object principal = authentication.getPrincipal(); if (principal instanceof UserDetails) { UserDetails userDetails = (UserDetails) principal; Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities(); return authorities.contains(new SimpleGrantedAuthority(request.getRequestURI())); } return false; } }
- 修改配置类
.anyRequest().access("@myServiceImpl.hasPermission(request,authentication)");
基于注解的访问控制
前提:通过@EnableGlobalMethodSecurity
注解开启用于访问控制的注解的功能
- @Secured:判断是否具有角色
- @PreAuthorize:在方法或者类执行之前先判断权限。该注解的value值可以是任何 access()支持的表达式
- @PostAuthorize:在方法或者类执行之后先判断权限。该注解的value值可以是任何 access()支持的表达式
RememberMe的功能实现
原理:用户在登录时添加remember me复选框。Spring security会自动将用用户信息存储到数据源中,以后就可以不登录进行访问。
- 添加依赖
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.15</version>
</dependency>
- 配置数据源
spring.datasource.driver-class-name= com.mysql.cj.jdbc.Driver
spring.datasource.url= jdbc:mysql://localhost:3306/security?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
spring.datasource.username= root
spring.datasource.password= 123456
- 编写配置
@Configuration
public class RememberMeConfig {
@Autowired
private DataSource dataSource;
@Bean
public PersistentTokenRepository getPersistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//自动建表,第一次启动时需要,第二次启动时注释掉
// jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
}
- 修改SecurityConfig配置
http.rememberMe()
// 失效时间,单位秒
.tokenValiditySeconds(120)
// 登录逻辑交给哪个对象
.userDetailsService(userDetailService)
// 持久层对象
.tokenRepository(persistentTokenRepository);
- 客户端登录页面添加remember-me的复选框
<input type="checkbox" name="remember-me" value="true"/><br/>
Spring Security中的CSRF
CSRF(Cross-Site request forgery):跨站请求伪造。
从 Spring Security4开始CSRF防护默认开启。默认会拦截请求。进行CSRF处理。CSRF为了保证不是其他第三方网站访问,要求访问时携带参数名为_csrf值为token(token在服务端产生)的内容,如果token和服务端的token匹配成功,则正常访问。
- 前台登录页面
- 保持开启CSRF防护(默认开启)