Spring Boot之整合Spring Security: 访
前言
在过往的一些Spring Boot学习项目中,我们会发现,我们开发的API都不需要认证,对所有人开放,连登录都不需要,毫无安全可言。
在项目实战中往往需要做好认证、授权、攻击防护,Spring Boot在这方面也提供了快速解决方案,即:推荐使用Spring Security。
-
Spring Boot为Spring Security提供了自动化配置方案,可零配置使用 Spring Security。
项目代码已上传Git Hub,欢迎取阅:
简单入门
1. 添加依赖;
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
2. 编写Controller;
package com.github.dylanz666.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author : dylanz
* @since : 08/30/2020
*/
@RestController
public class HelloController {
@GetMapping("/hello")
public String sayHello() throws Exception {
return "Hello!";
}
}
3. 启动项目并访问API;
- 启动项目:
注意一条log:Using generated security password: e10ac5ca-d3ab-4f0e-8e25-cbcf6afce611,下文会使用到。
启动项目- 在浏览器中访问API:
API如:http://127.0.0.1:8080/hello
默认登录页面 访问API-
输入用户名密码登录:
1). Username: 默认用户名为user;
2). Password: 默认密码为log中打印的密码,e10ac5ca-d3ab-4f0e-8e25-cbcf6afce611;
户名密码登录
我们一起来分析一下:
1). 未登录时访问API会重定向到登录页面:http://127.0.0.1:8080/login;
2). Spring Security为我们提供了默认的登录页面,登录页面还算美观;
3). 登录后,后续的请求中,会在请求头中带上含有JESSIONID的Cookie;
可在项目application.properties中提前配置好用户名和密码,如:
server.port=8080
spring.security.user.name=dylanz
spring.security.user.password=666
用户名密码登录
至此,我们就实现了最简单的登录认证。
自定义登录页面实例
- 未登录状态下API请求重定向到登录页面还是比较奇怪的,一般来说,API未登录状态下的请求应该显示状态码:401;
- 通常情况下,应该是进入某个有访问限制的页面,当未登录时,重定向到登录页面;
因此,我们将场景变为:
- 主页为(无需登录即可访问): http://127.0.0.1:8080/home.html
- 主页提供访问限制的入口;
- 有访问限制的页面为:http://127.0.0.1:8080/hello.html
- 登录页面为: http://127.0.0.1:8080/login.html
我们将采用视图技术,简单做个案例。Spring Boot框架内使用视图技术可参考:
thymeleaf使用准备:
1). 添加thymeleaf依赖;
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
2). 修改配置文件;
server.port=8080
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.mode=HTML
spring.thymeleaf.encoding=UTF-8
1. 定义主页home.html;
- 在resources下创建templates文件夹,并创建home.html文件:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<h1>Welcome!</h1>
<p>Click <a th:href="@{/hello.html}">here</a> to see a greeting.</p>
</body>
</html>
- 前往hello.html页面的代码:<a th:href="@{/hello.html}">here</a>
2. 定义hello.html页面;
- 在templates文件夹下创建hello.html文件:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"
xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out"/>
</form>
</body>
</html>
- hello.html页面上提供一个登出入口"Sign Out";
3. 自定义login/logout页面;
- 在templates文件夹下创建login.html文件:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"
xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example </title>
</head>
<body>
<div th:if="${param.error}">
Invalid username and password.
</div>
<div th:if="${param.logout}">
You have been logged out.
</div>
<form th:action="@{/login}" method="post">
<div><label th:style="'background:red;'"> User Name: <input type="text" name="username"/> </label></div>
<div><label th:style="'background:red;'"> Password: <input type="password" name="password"/> </label></div>
<div><input type="submit" value="Sign In"/></div>
</form>
</body>
</html>
- 当用户名密码错误时提示信息:Invalid username and password.
- 当登出时提示信息:You have been logged out.
- 为了演示自定义页面,我还特地改了下页面元素样式,把User Name和Password label的背景色改为红色:th:style="'background:red;'"
(笔者没有花过多的时间处理样式哈,此处只做简单演示)
4. 组织页面行为;
1). 配置模板匹配规则;
目的是使网站的url指向具体视图,而不是当作API来访问;
在项目下创建config包,并在config包内创建WebMvcConfig类,编写WebMvcConfig类如下:
package com.github.dylanz666.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author : dylanz
* @since : 08/30/2020
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/home.html").setViewName("home");
registry.addViewController("/").setViewName("home");
registry.addViewController("/hello.html").setViewName("hello");
registry.addViewController("/login.html").setViewName("login");
}
}
- 访问/和/home.html路径时,使用模板:home.html;
- 访问/hello.html路径时,使用模板:hello.html;
- 访问/login.html路径时,使用模板:login.html。
2). 页面访问权限设置;
在config包下创建类:WebSecurityConfig,编写类如下:
package com.github.dylanz666.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
/**
* @author : dylanz
* @since : 08/30/2020
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
.antMatchers("/", "/home.html").permitAll()//这2个url不用访问认证
.anyRequest().authenticated()//其他url都需要访问认证
.and()
.formLogin()
.loginPage("/login.html")//登录页面的url
.loginProcessingUrl("/login")//登录表使用的API
.permitAll()//login.html和login不需要访问认证
.and()
.logout()
.permitAll();//logout不需要访问认证
}
}
几点解释:
- @EnableWebSecurity:官网说这是为了开启Web Security支持,并提供Spring MVC集成,具体咋回事咱也不知道呀,跟着用就是对了!
- .antMatchers("/", "/home.html").permitAll():配置不需要认证的url,也即任何人都可以访问的url;
- .loginPage("/login.html"):配置登录页面的url,由于我们自定义了登录页面,因此需使用这个配置,如果不是用此配置,则使用Spring Security提供的默认登录页面;
- .loginProcessingUrl("/login"): 配置登录表单使用的API,Spring Security默认提供"/login"接口,用于登录验证;
3). 启动项目查看效果;
-
访问主页:http://127.0.0.1:8080/
-
点击页面中的"here"链接;
此时尝试访问http://127.0.0.1:8080/hello.html,但由于我们没有登录,因此Spring Security自动帮我们跳转到登录页面:http://127.0.0.1:8080/login.html
-
登录;
登录后
-
登录后访问项目写好的API;
笔者在项目中的controller包中写了个HelloController类,类中写了个get类型的API,代码如下:
package com.github.dylanz666.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author : dylanz
* @since : 08/30/2020
*/
@RestController
public class HelloController {
@GetMapping("/hello")
public String sayHello() throws Exception {
return "Hello!";
}
}
此时在浏览器中直接访问API:http://127.0.0.1:8080/hello
-
登出;
点击hello.html页面上的"Sign Out"按钮登出;
登出
此时退出到登录页面,且页面有提示信息:You have been logged out.
-
登出后访问项目写好的API;
再次在浏览器中直接访问API:http://127.0.0.1:8080/hello
此时我们会发现API被重定向到登录页面了;
通过本案例,我们学会了如何使用Spring Security进行基本的访问限制和自定义登录页面。
用户管理;
用户管理有几种方式:
1. 在resources底下的application.properties内配置可登录的用户信息:
spring.security.user.name=dylanz
spring.security.user.password=666
这种方式有个弊端:只能配置一个用户信息;
2. 在config底下的WebSecurityConfig配置类内添加可登录的用户信息userDetailsService,如:
package com.github.dylanz666.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
/**
* @author : dylanz
* @since : 08/30/2020
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
.antMatchers("/", "/home.html").permitAll()//这2个url不用访问认证
.anyRequest().authenticated()//其他url都需要访问认证
.and()
.formLogin()
.loginPage("/login.html")//登录页面的url
.loginProcessingUrl("/login")//登录表使用的API
.permitAll()//login.html和login不需要访问认证
.and()
.logout()
.permitAll();//logout不需要访问认证
}
@Bean
@Override
public UserDetailsService userDetailsService() {
UserDetails dylanz =
User.withUsername("dylanz")
.password(bCryptPasswordEncoder.encode("666"))
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user);
}
}
3. WebSecurityConfig配置类内可配置多个可登录的用户信息:
package com.github.dylanz666.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
/**
* @author : dylanz
* @since : 08/30/2020
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
.antMatchers("/", "/home.html").permitAll()//这2个url不用访问认证
.anyRequest().authenticated()//其他url都需要访问认证
.and()
.formLogin()
.loginPage("/login.html")//登录页面的url
.loginProcessingUrl("/login")//登录表使用的API
.permitAll()//login.html和login不需要访问认证
.and()
.logout()
.permitAll();//logout不需要访问认证
}
@Bean
@Override
public UserDetailsService userDetailsService() {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
UserDetails dylanz =
User.withUsername("dylanz")
.password(bCryptPasswordEncoder.encode("666"))
.roles("ADMIN")
.build();
UserDetails ritay =
User.withUsername("ritay")
.password(bCryptPasswordEncoder.encode("888"))
.roles("USER")
.build();
UserDetails jonathanw =
User.withUsername("jonathanw")
.password(bCryptPasswordEncoder.encode("999"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(dylanz, ritay, jonathanw);
}
}
我在WebSecurityConfig配置类内设置了3个可登录的用户,我们可以通过这种方式相对灵活的添加N个用户。
4. 在数据库中保存可登录的用户信息:
这是更常见的保存用户信息的方式,我们仍以最简单的方式来Demo从中心化的用户信息池获取用户信息,即:模拟数据库查询过程;
1). 项目下创建domain包、service包;
2). domain包内创建User实体类、service包下创建UserDetailsImpl类和UserDetailsServiceImpl类;
3). 编写User实体类;
package com.github.dylanz666.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;
import java.io.Serializable;
/**
* @author : dylanz
* @since : 08/31/2020
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
@Component
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private String password;
}
4). 编写UserDetailsImpl类;
package com.github.dylanz666.service;
import com.github.dylanz666.domain.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collection;
/**
* @author : dylanz
* @since : 08/31/2020
*/
@Service
public class UserDetailsImpl implements UserDetails {
private User currentUser;
public UserDetailsImpl() {
}
public UserDetailsImpl(User user) {
if (user != null) {
this.currentUser = user;
}
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
SimpleGrantedAuthority authority = new SimpleGrantedAuthority("admin");
authorities.add(authority);
return authorities;
}
@Override
public String getPassword() {
return currentUser.getPassword();
}
public String getUsername() {
return currentUser.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
5). 编写UserDetailsServiceImpl类;
package com.github.dylanz666.service;
import com.github.dylanz666.domain.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
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 org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author : dylanz
* @since : 08/31/2020
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private UserDetailsImpl userService;
@Autowired
private UserDetails userDetails;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//Spring Security要求必须加密密码
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
//模拟从数据库中取出用户信息,使用的sql如: SELECT * FROM USER WHERE USER_NAME='cherrys'
List<User> userList = new ArrayList<>();
User firstUser = new User();
firstUser.setUsername("cherrys");
firstUser.setPassword(passwordEncoder.encode("123"));
userList.add(firstUser);
User secondUser = new User();
secondUser.setUsername("randyh");
secondUser.setPassword(passwordEncoder.encode("456"));
userList.add(secondUser);
List<User> mappedUsers = userList.stream().filter(s -> s.getUsername().equals(username)).collect(Collectors.toList());
//判断用户是否存在
User user;
if (CollectionUtils.isEmpty(mappedUsers)) {
logger.info(String.format("The user %s is not found !", username));
throw new UsernameNotFoundException(String.format("The user %s is not found !", username));
}
user = mappedUsers.get(0);
return new UserDetailsImpl(user);
}
}
解释一下:
-
UserDetailsServiceImpl: 用于模拟从数据库查询出用户信息,且模拟数据库中存储了加密的字符串;
-
UserDetailsImpl:用于使用从数据库查询出的用户信息,设置可登录的用户名、密码,设置过程要配合使用WebSecurityConfig;
6). 修改WebSecurityConfig配置类;
package com.github.dylanz666.config;
import com.github.dylanz666.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
/**
* @author : dylanz
* @since : 08/30/2020
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
.antMatchers("/", "/home.html").permitAll()//这2个url不用访问认证
.anyRequest().authenticated()//其他url都需要访问认证
.and()
.formLogin()
.loginPage("/login.html")//登录页面的url
.loginProcessingUrl("/login")//登录表使用的API
.permitAll()//login.html和login不需要访问认证
.and()
.logout()
.permitAll();//logout不需要访问认证
httpSecurity.userDetailsService(userDetailsService());
httpSecurity.userDetailsService(userDetailsService);
}
@Bean
@Override
public UserDetailsService userDetailsService() {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
UserDetails dylanz =
User.withUsername("dylanz")
.password(bCryptPasswordEncoder.encode("666"))
.roles("ADMIN")
.build();
UserDetails ritay =
User.withUsername("ritay")
.password(bCryptPasswordEncoder.encode("888"))
.roles("USER")
.build();
UserDetails jonathanw =
User.withUsername("jonathanw")
.password(bCryptPasswordEncoder.encode("999"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(dylanz, ritay, jonathanw);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
特别注意:
-
必须在WebSecurityConfig中声明PasswordEncoder;
-
在WebSecurityConfig的configure方法中使用:
httpSecurity.userDetailsService(userDetailsService);
至此,我们在内存中添加了dylanz,ritay,jonathanw三个用户,并且数据库中也存储了cherrys、randyh两个用户,一共5个用户;
我们来测试一下:
randyh+正确密码1 randyh+正确密码2 randyh+错误密码 dylanz+正确密码1 dylanz+正确密码2 dylanz+错误密码 不存在的账户这个认证过程还是比较初级的,真实案例中会比这个认证过程复杂许多,我们开卷有益,再接再厉!
如果本文对您有帮助,麻烦动动手指点点赞?
谢谢!