1.spring security是什么?
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
2、添加依赖
首先,构建一个简单的Web工程,以用于后续添加安全控制。
本文采用的是Spring Boot + Thymeleaf的框架,使用Maven构建项目,储存用户名和密码的数据库采用的是H2 Database。
Spring Boot的入门和Spring Boot JPA访问H2 Database入门实现我们就不具体阐述了,需要的同学可以参考以下两个链接:
- Spring Boot快速入门
- Spring Boot JPA访问H2 Database
废话不多说,当构建完成一个基本的额Spring Boot项目后,首先,我们在pom.xml检查下是否都已经包含了如下配置,引入对Spring Security的依赖。pom.xml文件如下:
4.0.0
com.syf.demo
sprong-security-login
0.0.1-SNAPSHOT
UTF-8
1.8
org.springframework.boot
spring-boot-starter-parent
1.5.2.RELEASE
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-logging
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-thymeleaf
org.springframework.boot
spring-boot-starter-data-jpa
javax.persistence
persistence-api
1.0.2
com.h2database
h2
org.springframework.boot
spring-boot-maven-plugin
3.创建Java 配置类
Spring Security的接入方式一共有两种:基于注解方式和基于xml配置方式。
这里我们采用基于注解的方式,项目采用Spring Boot为基础。
除了最基本的Spring Boot配置之外,我么首先要创建Spring Security的Java 配置类。这里我们创建类SecurityConfiguration继承WebSecurityConfigurerAdapter,来对我们应用中所有的安全相关的事项(所有url,验证用户名密码,表单重定向等)进行控制。
3.1请求拦截策略
Spring security的请求拦截匹配有两种风格,ant风格和正则表达式风格。编码方式是通过重载configure(HttpSecurity)方法实现。
@Configuration
@EnableWebSecurity
@ComponentScan("sample.service")
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private TestingUserDetailService userDetailService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/css/**", "/fonts/**", "/js/**").permitAll()
.anyRequest().fullyAuthenticated().and()
.formLogin().loginPage("/login").failureUrl("/login?error").permitAll().and()
.logout().permitAll();
http.csrf().disable();
}
....
}
- @EnableWebSecurity: 禁用Boot的默认Security配置,配合@Configuration启用自定义配置(需要扩展WebSecurityConfigurerAdapter)
- configure(HttpSecurity) 通过重载,配置如何通过拦截器保护请求。
(1)通过authorizeRequests()定义哪些URL需要被保护、哪些不需要被保护。例如以上代码指定了访问css,js等静态资源的时候不需要任何认证就可以访问,其他的路径都必须通过身份验证。
(2)通过formLogin()定义当需要用户登录时候,转到的登录页面。
3.2 密码加密策略
通常我们在存储密码的时候都是进行加密的,spring security默认提供了三种密码存储方式,同时也可以使用自定义的加密方式:
1.NoOpPasswordEncoder 明文方式保存
2.BCtPasswordEncoder 强hash方式加密
3.StandardPasswordEncoder SHA-256方式加密
4.实现PasswordEncoder接口 自定义加密方式
这里我们采用自定义的加密方式在 configure(HttpSecurity http)后面添加 configure(AuthenticationManagerBuilder auth)来实现MD5+Salt的验证方式。
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setPasswordEncoder(passwordEncoder());
authProvider.setUserDetailsService(userDetailService);
ReflectionSaltSource saltSource = new ReflectionSaltSource();
saltSource.setUserPropertyToUse("salt");
authProvider.setSaltSource(saltSource);
auth.authenticationProvider(authProvider);
}
@Bean
public Md5PasswordEncoder passwordEncoder() {
return new Md5PasswordEncoder();
}
- configure(AuthenticationManagerBuilder) 通过重载,配置user-detail服务,这里通过配置身份验证,注入自定义身份验证Bean和密码校验规则(MD5+Salt)。
4.用户存储认证方式
Spring security提供了多种用户存储认证方式:
- 使用基于内存的用户存储
@Override
public void configure(AuthenticationManagerBuilder auth)throws Exception{
//基于内存的用户存储、认证
auth.inMemoryAuthentication()
.withUser("admin").password("admin").roles("ADMIN","USER")
.and()
.withUser("user").password("user").roles("USER");
}
- 基于数据库表用户存储认证
@Override
public void configure(AuthenticationManagerBuilder auth)throws Exception{
//基于数据库的用户存储、认证
auth.jdbcAuthentication().dataSource(dataSource)
.usersByUsernameQuery("select account,password,true from user where account=?")
.authoritiesByUsernameQuery("select account,role from user where account=?");
}
- 配置自定义的用户存储认证
本文采用的就是配置自定义的用户存储认证,自定义的方式也很简单。只需要提供一个UserDetailService接口实现即可。在密码加密策略中用到的userDetailService就是本文自定义的用户存储认证,其代码如下:
@Service
public class TestingUserDetailService implements UserDetailsService {
private static final Logger logger = LoggerFactory.getLogger(TestingUserDetailService.class);
@Autowired
private UserRepository userRepository;
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
logger.debug("Load user by username: " + userName);
List authorities = new ArrayList();
User user = userRepository.findByUserName(userName);
if (user == null) {
logger.error("User not found");
throw new UsernameNotFoundException("User not found");
}
authorities.add(new SimpleGrantedAuthority(user.getRole().toString()));
return new UserWithSalt(user.getUserName(), user.getUserName(), user.getPassword(),
authorities);
}
}
自定义的方式只要实现接口方法loadUserByUsername(String username)即可,返回代表用户的UserDetails对象。这里我们返回的是UserWithSalt对象,使用用户名作为“Salt值”来混淆,实现密码的MD5+Salt加密,该对象的具体实现是:
public UserWithSalt(String username, String salt, String password,
Collection extends GrantedAuthority> authorities) {
super(username, password, authorities);
this.salt = salt;
}
User实体类的具体信息如下:
@Entity
@Table(name = "user")
public class User implements Serializable{
private static final long serialVersionUID = 8831737280677584496L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(name = "user_name", unique = true, nullable = false)
private String userName;
@Column(name = "password", nullable = false)
private String password;
@Column(name = "role", nullable = false)
@Enumerated(EnumType.STRING)
private Role role;
@Column(name = "active", nullable = false)
private boolean active;
protected User() {}
/**
* Constructor.
*
* @param userName String
* @param password String
* @param role Role
* @param active Boolean
*/
public User(String userName, String password, Role role, boolean active) {
super();
this.userName = userName;
this.password = password;
this.role = role;
this.active = active;
}
@Override
public String toString() {
return String.format("User[id=%d, name='%s', password='%s', role='%s', active='%b']", id,
userName, password, role, active);
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Role getRole() {
return role;
}
public void setRole(Role role) {
this.role = role;
}
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}
5. Web层实现请求映射
这里我提供了两种方法:
1.传统的controller来控制跳转,大部分应用场景中View和后台都会有数据交互,所以采用这种方式比较普遍。
2.通过WebMvcConfigurerAdapter来实现,想通过一个URL Mapping然后不经过Controller处理直接跳转到页面上。
@Controller
public class MainController {
@RequestMapping(value = "/", method = RequestMethod.GET)
public String home(Map model) {
model.put("message", "Hello World");
model.put("title", "Hello Home");
model.put("date", new Date());
return "home";
}
@RequestMapping("/foo")
public String foo() {
throw new RuntimeException("Expected exception in controller");
}
}
@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("login");
}
}
通过上面的配置,不用添加LoginController或者处理“login”的方法就可以直接通过"/login"访问到login.html页面了!
6.实现映射的页面
src\main\resources\templates\login.html
Login
Login with Username and Password
You have been logged out
There was an error, please try again
src\main\resources\templates\home.html
Title
Fake content
July 11, 2012 2:17:16 PM CDT
本文通过一个非常简单的示例完成了对Web应用的安全控制,从数据库中读取用户名和密码信息,并利用加密算法提高了安全性,但是Spring Security提供的功能还远不止于此,更多Spring Security的使用可参见Spring Security Reference。
完整示例:
Demo Github 地址