Spring Security最主要的作用就是就是认证安全和登录鉴权。Spring Security其核心就是一组过滤器链,项目启动后将会自动配置。最核心的就是 Basic Authentication Filter 用来认证用户的身份,一个在Spring Security中一种过滤器就处理一种认证方式,一种过滤器按照自身职责判定是否是自身需要的信息,认证通过就进入下一环节,直至最后认证通过。
1. Spring Security基本使用体验
创建Spring boot项目,首先不引入Spring Security框架,创建最简单的web项目。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
添加两个基础的页面,在template文件夹下,创建home.html页面。
home.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>home页面</title>
</head>
<body>
<h1>Welcome!,来到home页面</h1>
<p>
Click
<a th:href="@{/hello}">here</a>
to see a greeting.
</p>
</body>
</html>
hello.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>hello页面</title>
</head>
<body>
<h1>Hello world!</h1>
</body>
</html>
点击home页面中的 “/hello” 链接跳转到 hello页面。这里首先需要添加控制器,完成跳转,这里就配置视图控制器来暴露这些页面。
/**
* spring mvc 视图控制器
*/
@Configuration
public class MvcConfig implements WebMvcConfigurer {
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/home").setViewName("home");
registry.addViewController("/").setViewName("home");
registry.addViewController("/hello").setViewName("hello");
}
}
启动项目,就可以通过http://localhost:8080/home访问home页面,然后通过该页面的链接跳转至hello页面。
接着加入Spring Security依赖,为项目添加安全权限功能。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
直接引入依赖后,Spring Security就已经起作用了;启动项目后,再次访问http://localhost:8080/home,会直接跳转到Spring Security自动生成的login页面。可以看见我们自己创建的页面接口已经被保护起来了。
可以通过Spring Security进行安全配置,可以使得只有认证过的用户才可以访问到受保护的页面,也可以指定哪些页面访问可以放行。
/**
* spring security 安全配置项
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/", "/home")
.permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.passwordEncoder(new BCryptPasswordEncoder())
.withUser("user")
.password(new BCryptPasswordEncoder().encode("123456"))
.roles("USER");
}
}
WebSecurityConfig类使用了@EnableWebSecurity注解 ,以启用Spring Security的Web 安全支持。
configure() 方法定义了哪些URL路径应该被保护,哪些不需要保护直接放行。“/” 和 “/home” 路径被配置为不需要任何身份验证。其余的路径必须经过身份验证才能访问。 当用户成功登录时,它们将被重定向到先前请求的需要身份认证的页面。
configureGlobal() 方法,它将单个用户设置在内存中。该用户的用户名为 “user”,密码为 “123456”,角色为 “USER”。
创建login.html页面,用于替代自定义的login页面。
<!DOCTYPE html>
<html
xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://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>
User Name :
<input type="text" name="username" />
</label>
</div>
<div>
<label>
Password:
<input type="password" name="password" />
</label>
</div>
<div><input type="submit" value="Sign In" /></div>
</form>
</body>
</html>
启动项目后,访问http://localhost:8080/login。
该页面提供了一个表单来获取用户名和密码,并将它们提交到 “/login”。 根据前面的配置,Spring Security提供了一个拦截该请求并验证用户的过滤器。 如果用户未通过认证,该页面将重定向到 “/login?error”,并在页面显示相应的错误消息;如果用户通过认证就会进入home页面。
前面添加了注销登录配置,logout() 方法就是用户注销功能。注销成功后,应用程序会默认跳转到 “/login?logout”,就是前面的登录页面。修改下 hello.html 页面,添加一个logout链接。
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out"/>
</form>
可以看到点击退出登陆就会跳转至login页面,到这里Spring Security最基本的使用流程就完成了。
2.用户认证
进行用户认证,第一步就是需要获取用户的信息。Spring Security提供了 UserDetailsService接口来获取用户信息。这个接口用户加载用户的信息,提供了一个只读方法,便于简化对新的访问策略的支持。
创建MyUserDetailsService实现UserDetailsService,实现读取用户信息的方法。这样就能让自定义的UserdetailsService生效了,接着我们就可以从这里获取用户的信息。
在WebSecurityConfig中声明对密码加密的类。
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
自定义数据源来获取数据
这里只要是存在一个自定义的UserDetailsService,那么Spring Security将会使用该实例进行配置
*/
@Component
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
// 可以从任何地方获取数据,比如内存中或者数据库
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("获取到的用户信息:" + username);
String password = passwordEncoder.encode("123456");
return new User(username, password,
AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
去除前面设置在内存中的用户信息,此时这里不再从哪里获取,而是从自定义的MyUserDetailsService中获取。
启动项目就可以使用admin/123456进行登陆了。
2.1 前后端分离模式的用户认证
在前面登录验证时都是统一跳转到了一个登录的html页面上去;在前后分离的情况下,一般都是给前端一个JSON串,让前端人员去判断处理。具体的处理方法是在完成身份验证后,不再直接跳转到页面,而是跳转到Controller中。在Controller中完成后续的逻辑。
添加依赖
<!--json相关依赖-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.49</version>
</dependency>
<!--lombok组件-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.0</version>
</dependency>
修改Spring Security的安全配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/authentication/*", "/", "/home", "/login")
.permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
//.loginPage("/login")
// 更换成自定义的一个真实存在的处理器地址
.loginPage("/authentication/require")
.loginProcessingUrl("/authentication/form")
// 加入自定义处理器
.successHandler(myAuthenticationSuccessHandler)
.permitAll()
.and()
// csrf 防护关掉
.csrf().disable()
.logout()
.permitAll();
}
这里不再是跳转至login页面了,而是跳转至一个处理地址,需要创建一个控制器处理处理这个请求。
创建RestResult.java用于封装结果返回信息。
public class RestResult {
// 定义jackson对象
private static final ObjectMapper MAPPER = new ObjectMapper();
private String status;
private String msg;
private Object data;
public static RestResult build(String status, String msg, Object data) {
return new RestResult(status, msg, data);
}
public static RestResult ok(Object data) {
return new RestResult(data);
}
public static RestResult ok() {
return new RestResult(null);
}
public RestResult() {
}
public static RestResult build(String status, String msg) {
return new RestResult(status, msg, null);
}
public RestResult(String status, String msg, Object data) {
this.status = status;
this.msg = msg;
this.data = data;
}
public RestResult(Object data) {
this.status = "200";
this.msg = "OK";
this.data = data;
}
public Boolean isOK() {
return this.status == "200";
}
/**
* 有object对象的转化
* @param jsonData
* @param clazz
* @return
*/
public static RestResult formatToPojo(String jsonData, Class<?> clazz) {
try {
if (clazz == null) {
return MAPPER.readValue(jsonData, RestResult.class);
}
JsonNode jsonNode = MAPPER.readTree(jsonData);
JsonNode data = jsonNode.get("data");
Object obj = null;
if (clazz != null) {
if (data.isObject()) {
obj = MAPPER.readValue(data.traverse(), clazz);
} else if (data.isTextual()) {
obj = MAPPER.readValue(data.asText(), clazz);
}
}
return build(jsonNode.get("status").asText(), jsonNode.get("msg").asText(), obj);
} catch (Exception e) {
return null;
}
}
/**
* 没有object对象的转化
* @param json
* @return
*/
public static RestResult format(String json) {
try {
return MAPPER.readValue(json, RestResult.class);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* Object是集合转化
* @param jsonData
* @param clazz
* @return
*/
public static RestResult formatToList(String jsonData, Class<?> clazz) {
try {
JsonNode jsonNode = MAPPER.readTree(jsonData);
JsonNode data = jsonNode.get("data");
Object obj = null;
if (data.isArray() && data.size() > 0) {
obj = MAPPER.readValue(data.traverse(),
MAPPER.getTypeFactory().constructCollectionType(List.class, clazz));
}
return build(jsonNode.get("status").asText(), jsonNode.get("msg").asText(), obj);
} catch (Exception e) {
return null;
}
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
创建SecurityRequestController.java处理/authentication/require请求。
@RestController
public class SecurityRequestController {
private RequestCache requestCache = new HttpSessionRequestCache();
// spring的工具类:封装了所有跳转行为策略类
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
/**
* 当需要身份认证时跳转到这里
*/
@RequestMapping("/authentication/require")
@ResponseStatus(code = HttpStatus.UNAUTHORIZED)
public RestResult requirAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
SavedRequest savedRequest = requestCache.getRequest(request, response);
// 如果有引发认证的请求
if (savedRequest != null) {
String targetUrl = savedRequest.getRedirectUrl();
System.out.println(("引发跳转的请求:" + targetUrl));
// 如果是html请求,则跳转到登录页
if (StringUtils.endsWithIgnoreCase(targetUrl, ".html")) {
redirectStrategy.sendRedirect(request, response, "/login");
}
}
// 否则都返回需要认证的json串
return new RestResult("访问受限,请前往登录页面");
}
}
创建自定义处理器MyAuthenticationSuccessHandler.java
/**
* formLogin().successHandler() 中需要的处理器类型
*/
@Component("myAuthenticationSuccessHandler")
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
// spring 是使用jackson来进行处理返回数据的
// 所以这里可以得到他的实例
@Autowired
private ObjectMapper objectMapper;
private final static String LoginType = "JSON";
/**
* @param authentication 封装了所有的认证信息
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
if (LoginType == "JSON") {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(authentication));
} else {
super.onAuthenticationSuccess(request, response, authentication);
}
}
}
修改home页面的内容。
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>home页面</title>
</head>
<body>
<h1>Welcome!,来到home页面</h1>
<p>
Click
<a th:href="@{/hello.html}">here</a>
to see a greeting.
</p>
</body>
</html>
修改视图控制器。
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/home").setViewName("home");
registry.addViewController("/").setViewName("home");
registry.addViewController("/hello.html").setViewName("hello");
registry.addViewController("/login").setViewName("login");
}
启动项目,访问http://localhost:8080/home,会跳转至home页面,点击链接就会跳转至登陆页面。这里请求带html后缀就会跳转至login页面。
接着使用非html后缀的请求,这里就会返回json字符串。
这里的/authentication/form是登陆请求,我们使用postman进行登陆访问,注意这里是post请求。
demo源码下载:demo源码