Spring Security快速上手
Spring Security介绍
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架.
创建工程
在security项目的基础上增加spring-security依赖:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.3.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.3.2.RELEASE</version>
</dependency>
Spring容器配置
同security项目一样:
package com.cehcloud.cehc.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
/**
* @author Lenovo
* @date 2020/8/18 21:25
*/
@Configuration
@ComponentScan(basePackages = "com.cehcloud.cehc"
,excludeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class)})
public class ApplicationConfig {
// 在此配置除了Controller的其它bean,比如:数据库、事务管理器、业务bean等
}
Servlet Context配置
同security项目一样:
package com.cehcloud.cehc.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;
/**
* @author Lenovo
* @date 2020/8/18 21:29
*/
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.cehcloud.cehc"
,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("login");
}
}
加载Spring容器
在init包下定义Spring容器初始化类SpringApplicationInitializer,此类实现WebApplicationInitializer接口,Spring容器启动时加载WebApplicationInitializer接口的所有实现类.
package com.cehcloud.cehc.init;
import com.cehcloud.cehc.config.ApplicationConfig;
import com.cehcloud.cehc.config.WebConfig;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
/**
* @author Lenovo
* @date 2020/8/18 21:45
*/
public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
/**
* Spring容器,相当于加载applicationContext.xml
* @return
*/
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{
ApplicationConfig.class};
}
/**
* servletContext,相当于加载springmvc.xml
* @return
*/
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{
WebConfig.class};
}
/**
* url-mapping
* @return
*/
@Override
protected String[] getServletMappings() {
return new String[]{
"/"};
}
}
认证
认证页面
spring-security默认提供了认证页面,不需要额外开发
安全配置
spring-security提供了用户名密码登录、退出、会话管理等认证功能,只需要配置即可使用。
(1)在config包下定义WebSecurityConfig,安全配置内容的内容包括:用户信息、密码编辑器、安全拦截机制。
package com.cehcloud.cehc.config;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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;
import javax.servlet.http.HttpSession;
/**
* @author Lenovo
* @date 2020/8/19 15:49
*/
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 定义用户信息服务
* @return
*/
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("zhangsan").password("123").authorities("v1").build());
manager.createUser(User.withUsername("lisi").password("12345").authorities("v2").build());
return manager;
}
/**
* 密码编辑器
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
/**
* 配置安全拦截机制
* @param httpSecurity
* @throws Exception
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
.antMatchers("/t/**").authenticated()
.anyRequest().permitAll()
.and()
.formLogin()
.successForwardUrl("/login-success");
}
}
(2)加载WebSecurityConfig
/**
* Spring容器,相当于加载applicationContext.xml
* @return
*/
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{
ApplicationConfig.class, WebSecurityConfig.class};
}
Spring Security初始化
spring security初始化有两种情况:
- 若当前环境没有使用Spring或SpringMVC,则需要将WebSecurityConfig(Spring Security配置类)传入超类,以确保获取配置,并创建spring context.
- 相反,若当前环境已经使用spring,我们应该在现有的SpringContext中注册Spring Security(上一步已经将WebSecurityConfig加载至rootcontext),此方法可以什么都不做.
在init包下定义SpringSecurityApplicationInitializer:
package com.cehcloud.cehc.init;
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
/**
* @author Lenovo
* @date 2020/8/19 16:05
*/
public class SpringSecurityApplicationInitializer
extends AbstractSecurityWebApplicationInitializer {
public SpringSecurityApplicationInitializer(){
}
}
默认根路径请求
在WebConfig.java中添加默认请求根路径跳转到/login,此url为spring security提供:
/**
* 默认url根路径跳转到/login吃url为spring security提供
* @param registry
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("redirect:login");
}
认证成功的页面
在安全配置中,认证成功将跳转到/login-success,代码如下:
/**
* 配置安全拦截机制
* @param httpSecurity
* @throws Exception
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
.antMatchers("/t/**").authenticated()
.anyRequest().permitAll()
.and()
.formLogin()
.successForwardUrl("/login-success");
}
spring security支持表单验证,认证成功后转向/login-success.
在LoginController中定义/login-success:
/**
* 登录成功页面
* @return
*/
@RequestMapping(value = "/login-success", produces = "text/plain;charset=utf-8")
public String loginSuccess() {
return "登录成功";
}
授权
在LoginController中添加/t/t1或/t/t2
package com.cehcloud.cehc.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author Lenovo
* @date 2020/8/19 16:16
*/
@RestController
public class LoginController {
/**
* 登录成功页面
* @return
*/
@RequestMapping(value = "/success", produces = {
"text/plain;charset=UTF-8"})
public String loginSuccess() {
return "登录成功";
}
/**
* 访问资源1
* @return
*/
@GetMapping(value = "/t/t1", produces = {
"text/plain;charset=UTF-8"})
public String r1() {
return "访问资源1";
}
/**
* 访问资源2
* @return
*/
@GetMapping(value = "/t/t2", produces = {
"text/plain;charset=UTF-8"})
public String r2() {
return "访问资源1";
}
}
spring Security 应用详解
集成SpringBoot
spring容器配置
SpringBoot工程启动会自动扫描启动类所在包下的所有Bean,加载到spring容器.
(1)Spring配置文件
server:
port: 8080
servlet:
context-path: /spring-boot-security
spring:
application:
name: spring-boot-security
(2)Spring Boot启动类
package com.cehcloud.cehc.security;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityApplication.class, args);
}
}
Servlet Context配置
由于Spring Boot starter自动装配机制,这里无需使用@EnableWebMvc与@ComponenetScan,WebConfig如下:
package com.cehcloud.cehc.security.config;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author Lenovo
* @date 2020/8/20 13:07
*/
public class WebConfig implements WebMvcConfigurer {
/**
* 默认URL根路径跳转到/login此url为spring security提供
* @param registry
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("redirect:/login");
}
}
视图解析器配置在全局配置文件中:
server:
port: 8080
servlet:
context-path: /spring-boot-security
spring:
application:
name: spring-boot-security
mvc:
view:
prefix: /templates/
suffix: .jsp
安全配置
由于SpringBoot的自动装配机制,这里无需要@EnableWebSecurity注解,WebSecurityConfig内容如下:
package com.cehcloud.cehc.security.config;
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.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
/**
* @author Lenovo
* @date 2020/8/19 15:49
*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 定义用户信息服务
* @return
*/
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("123").password("123").authorities("v1").build());
manager.createUser(User.withUsername("123456").password("123456").authorities("v2").build());
return manager;
}
/**
* 密码编辑器
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
/**
* 配置安全拦截机制
* @param httpSecurity
* @throws Exception
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeRequests()
.antMatchers("/t/**").authenticated()
.anyRequest().permitAll()
.and()
.formLogin()
.successForwardUrl("/success");
}
}
工作原理
认证流程
AuthenticationProvider
AuthenticationProvider是一个接口,定义如下:
public interface AuthenticationProvider {
Authentication authenticate(Authentication authenticatiion) throws AuthenticationException;
boolean supports(Class<?> var1);
}
authenticate()方法定义了认证的实现过程,它的参数是一个Authentication,里面包含了登录用户的用户名、密码。而返回值也是一个Authentication,这个Authentication则是在认证成功后,将用户的权限及其它的信息重新组装后生成。
Spring Security中维护着一个List列表,存放多种认证方式,不同的认证方式使用不同的AuthenticationProvider.
每个AuthenticationProvider需要实现supports()方法来表名自己支持的认证方式,如我们使用表单方式认证,在提交请求时Spring Security会生成UsernamePasswordAuthenticationToken,它是一个Authentication,里面封装着用户提交的用户名、密码等信息,而对象的,哪个AuthenticationProvider来处理它。
我们在DaoAuthenticationProvider的基类AbstractUserDetailsAuthenticationProvider发现以下代码:
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableForm(authentication);
}
也就是说当web表单提交用户名密码时,Spring Security由DaoAuthenticationProvider处理.
最后,我们来看一下Authentication(认证信息)的结构,它是一个接口,我们之前提到的UsernamePasswordAuthenticationToken就是它的实现之一.
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean varl) throws IllegalArgumentException;
}
UserDetailsService
现在我们知道DaoAuthenticationProvider处理了Web表单的认证逻辑,认证成功后即得到一个Authentication(UsernamePasswordAuthenticationToken实现),里面包含了身份信息,这个身份信息就是一个Object,大多数情况下被强制转为UserDetials对象.
DaoAuthenticationProvider中包含了UserDetialsService实例,他负责根据用户名提取用户信息,而后DaoAuthenticationProvider会去对比UserDetailsService提取的用户密码与用户提交的密码时候匹配作为认证的关键依据.
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
Spring Security 提供的InMemoryUserDetailsManager(内存认证),JdbcUserDetailsManager(jdbc认证)就是UserDetailsService的实现类,主要区别无非是从内存还是从数据库加载用户.
自定义认证
导入依赖
因为SPringBoot不提倡用jsp所以在使用的时候需要导入以下依赖:
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
</dependency>
配置认证页面
在WebConfig中配置认证页面地址
package com.cehcloud.cehc.security.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author Lenovo
* @date 2020/8/20 13:07
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* 默认URL根路径跳转到/login此url为spring security提供
* @param registry
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("redirect:/login-view");
registry.addViewController("/login-view").setViewName("login");
}
}
安全配置
在WebSecurityConfig中配置表单登录信息
/**
* 配置安全拦截机制
* @param httpSecurity
* @throws Exception
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity .csrf().disable()
.authorizeRequests()
.antMatchers("/t/**").authenticated()
.anyRequest().permitAll()
.and()
.formLogin()
.loginPage("/login-view")
.loginProcessingUrl("/login")
.successForwardUrl("/success")
.permitAll();
}
问题解决
spring Security为了防止CSRF的发生,限制了除了get以外的大多数方法.
解决方法一
屏蔽CSRF控制,即spring Security不再限制CSRF.
配置WebSecurityConfig
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity .csrf().disable()
...
}
解决方法二
在登录页面添加一个token,spring security会验证token,如果token合法则可以继续请求.
修改login.jsp
<%--
Created by IntelliJ IDEA.
User: Lenovo
Date: 2020/8/19
Time: 13:53
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>登录</title>
</head>
<body>
<form action="login" method="post">
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
用户名:<input type = "text" name = "username"/><br>
密码:<input type="password" name="password"/><br>
<input type="submit" value="登录">
</form>
</body>
</html>
连接数据库认证
创建数据库
创建user_db数据库
CREATE DATABASE `user_db`CHARACTER SET utf8 COLLATE utf8_general_ci;
创建t_user表
CREATE TABLE `t_user`(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '用户id',
`username` VARCHAR(64) NOT NULL,
`password` VARCHAR(64) NOT NULL,
`fullname` VARCHAR(255) NOT NULL COMMENT '用户姓名',
`mobile` VARCHAR(11) NOT NULL COMMENT '手机号',
PRIMARY KEY (`id`)
) ENGINE=INNODB CHARSET=utf8;
代码实现
添加数据源
server:
port: 8080
servlet:
context-path: /spring-boot-security
spring:
application:
name: spring-boot-security
mvc:
view:
prefix: /WEB-INF/view/
suffix: .jsp
datasource:
url: mysql://localhost:3306/user_db
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
添加依赖:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
定义Dao
在定义模型类型,在model包定义UserDTO:
package com.cehcloud.cehc.security.model;
import lombok.Data;
/**
* @author Lenovo
* @date 2020/8/20 15:52
*/
@Data
public class UserDTO {
/**
* 用户ID
*/
private String id;
/**
* 用户名
*/
private String username;
/**
* 用户密码
*/
private String password;
/**
* 用户姓名
*/
private String fullname;
/**
* 用户电话
*/
private String mobile;
}
在Dao包定义UserDAO
package com.cehcloud.cehc.security.dao;
import com.cehcloud.cehc.security.model.UserDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* @author Lenovo
* @date 2020/8/20 15:57
*/
@Repository
public class UserDAO {
@Autowired
private JdbcTemplate jdbcTemplate;
public UserDTO getUserByUsername(String username) {
String sql = "select id, username, password, fullname, mobile from t_user where username = ?";
List<UserDTO> list = jdbcTemplate.query(sql, new Object[]{
username}, new BeanPropertyRowMapper<>(UserDTO.class));
if (list == null && list.size() == 1) {
return null;
}
return list.get(0);
}
}