一、Spring Security 的原理介绍
Spring Security 是基于spring和Servlet规范中的Filter实现的,使用Filter保护web请求并心智URL级别的访问,为企业应用系统提供声明式的安全访问控制的安全框架,它提供了一组可以在spring应用上下文配置的Bean,充分利用了Spring IOC 、DI和AOP的功能,为应用系统提供声明式的安全访问控制功能,减少了大量的重复代码编写。它是一个轻量级的安全框架,主要功能是提供身份验证、授权支持和攻击防护,包括两个安全操作-----“认证”和“验证”。为用户建立一个声明的角色过程,该角色可以是一个用户、一个设备或者一个系统,这个概念叫做认证。基于AOP保护方法,某一用户在应用系统中执行某个方法需要判断是否具备访问权限,这个概念叫做验证。
理解AOP,补充:代理模式
什么是代理模式?为其他对象提供一种代理以控制对这个代理对象的访问。代理对象就是增加对象的功能的。
1.1 静态代理:代理对象要维护一个目标对象,就是实现类的接口。即目标对象必须实现接口,而代理对象要实现与目标对象的一样的接口。试想,如果目标对象增加一个方法,实现类也要跟着实现该方法,代理类也要实现改的方法以增强该方法的功能。
1.2 JDK 生成的动态代理:与静态代理不同,动态代理类的源码是在程序运行期间通过JVM反射等机制动态生成,运行时确定了代理类和被代理类的关系。给多个目标对象生成代理对象。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import cn.itcast.service.UserService;
import cn.itcast.service.UserServiceImpl;
public class UserServiceProxyFactory implements InvocationHandler{
//使用构造方法必须把us传进来
public UserServiceProxyFactory(UserService us) {
super();
this.us = us;
}
private UserService us;
public UserService getUserServiceProxy() {
//生成动态接口代理
UserService usProxy = (UserService) Proxy.newProxyInstance(UserServiceProxyFactory.class.getClassLoader(),
UserServiceImpl.class.getInterfaces(), this);
return usProxy;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("打开事务");
Object invoke = method.invoke(us, args);
System.out.println("提交事务");
return invoke;
}
}
1.3 cglib代理:
CGLIB 是以动态生成的子类继承目标的方式实现,在运行期动态的在内存中构建一个子类。CGLIB 使用的前提是目标类不能为 final 修饰。因为 final 修饰的类不能被继承。
mport java.lang.reflect.Method;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import cn.itcast.service.UserService;
import cn.itcast.service.UserServiceImpl;
/**
* 观光代码 cglib代理
* @author quan
*
*/
public class UserServiceProxyFactory2 implements MethodInterceptor{
public UserService getUserServiceProxy() {
Enhancer en = new Enhancer();
en.setSuperclass(UserServiceImpl.class);
en.setCallback(this);
UserService us = (UserService) en.create();
return us;
}
@Override
public Object intercept(Object proxyobj, Method menhtod, Object[] arg2, MethodProxy methodProxy) throws Throwable {
// 打开事务
System.out.println("打开事务");
// 调用原有方法
Object invokeSuper = methodProxy.invokeSuper(proxyobj, arg2);
// 提交事务
System.out.println("提交事务");
return invokeSuper;
}
}
测试类:
public class Demo {
//手写AOP,哈哈哈
@Test
public void fun1() {
UserService us = new UserServiceImpl();
UserServiceProxyFactory factory = new UserServiceProxyFactory(us);
UserService userServiceProxy = factory.getUserServiceProxy();
userServiceProxy.delete();
}
@Test
public void fun2() {
UserService us = new UserServiceImpl();
UserServiceProxyFactory2 factory = new UserServiceProxyFactory2();
UserService userServiceProxy = factory.getUserServiceProxy();
userServiceProxy.delete();
}
}
回归AOP,看看 AOP 的定义:面向切面编程,核心原理是使用动态代理模式在方法执行前后或出现异常时加入相关逻辑。
AOP 是基于动态代理模式。
AOP 是方法级别的。
AOP 可以分离业务代码和关注点代码(重复代码),在执行业务代码时,动态的注入关注点代码。切面就是关注点代码形成的类。
1.4 重温Spring AOP
前文提到 JDK 代理和 CGLIB 代理两种动态代理。优秀的 Spring 框架把两种方式在底层都集成了进去,我们无需担心自己去实现动态生成代理。那么,Spring是如何生成代理对象的?
创建容器对象的时候,根据切入点表达式拦截的类,生成代理对象。
如果目标对象有实现接口,使用 JDK 代理。如果目标对象没有实现接口,则使用 CGLIB 代理。然后从容器获取代理后的对象,在运行期植入“切面”类的方法。通过查看 Spring 源码,我们在 DefaultAopProxyFactory 类中发现:
如果有接口,则使用 JDK 代理,反之使用 CGLIB ,这刚好印证了前文所阐述的内容。Spring AOP 综合两种代理方式的使用前提有会如下结论:如果目标类没有实现接口,且 class 为 final 修饰的,则不能进行 Spring AOP 编程!
二、过滤web请求
2.1 安全性配置
DelegatingFilterProxy把Filter的处理逻辑委托给spring上下文中的java.servlet.Filter的实现类,它自己的工作量很少。有web.xml和java的方式来配置DelegatingFilterProxy。springSecurityFilterChain本身也是特殊的Filter,被称为FilterChainProxy,能够连接一个或多个Filter,启动web安全性的时候自动创建这些Filter。
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
借助WebApplicationInitializer以Java的方式配置DelegatingFilterProxy
public class SecurityWebInitializer extends AbstractSecurityWebApplicationInitializer{ }
实现了WebApplicationInitializer后,spring会发现它,并用它在web容器中注册DelegatingFilterProxy。无论哪种配置,都会拦截发往应用中的请求,并将请求委托给ID为springSecurityFilterChain的bean。
spring3.2引入了新的Java配置方案:
@Configuration
@EnableWebSecurity //可以启动任意web应用的安全性功能
public class SecurityConfig extends WebSecurityConfigurerAdapter {}
如果应用正好是springMVC开发的,应该考虑使用@EnableWebMvcSecurity注解,能够解析@AuthenticationPrincipal注解的参数,获得认证用户的principal或者username,同时还配置了一个bean,在使用spring表单绑定标签库来定义表单时,这个bean自动添加一个隐藏的跨站请求伪造csrf的token输入域。
@Configuration
@EnableWebMvcSecurity //启动SpringMVC安全性
public class SecurityConfig extends WebSecurityConfigurerAdapter {}
2.2 Spring Security基于各种数据存储来进行认证用户:基于内存的用户存储、基于数据库表进行认证、基于LDAP进行认证
2.2.1基于内存的用户存储
@Configuration
@EnableWebMVCSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
auth
.inMemoryAuthtication()
.withUser("user").password("password").roles("USER").and()
.withUser("admin").password("password").roles("USER","ADMIN");//多角色
}
}
2.2.2 基于数据库表的认证,按照配置自己的查询方式,所有的查询都将用户名作为唯一的参数,Spring Security的加密模块好阔了3个实现:BCryptPasswordEncoder、NoOpPasswordEncoder、StandardPasswordEncoder
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
auth
.jdbcAuthetication()
.dataSource(datasource)
.userByUsernameQuery(
"select username,password,ture"+
"from 表名 where username=?"
)
.authoritiesByUsernameQuery(
"select uername,'ROLE_USER' from 表名 where username=?")
.passwordEncoder(new StandardPasswordEncoder("53cr3t"));
}
也可以提供自定义的实现,PasswordEncoder接口很简单;
public interface PasswordEncoder{
String encode(CharSequence rawpassword);
boolean matches(CharSequence rawpassword, String encodepassword);
}
2.2.3 基于LDAP进行认证(暂时省略)
2.2.4 非关系型数据库的用户认证
需要提供一个自定义的UserDetailsService接口实现,
public class xxUserService impletements UserDetailsService{
private final XXRepository xxRepository;
构造方法注入;
@Override
public UserDetails loadUserByUsername(String username)
throws UsrenameNotFoundException{
Student student = xxRepository.findByUsername(username);
if(res != null){
List<GrantedAuthority> authotities = new ArrayList<GrantedAuthority>();
authorities.add(new SimpleGrantedAuthority("ROLE_STUDENT"));
return new User(student.getUsername(),student.getPassword(),authroties);
}
throw new UsrenameNotFoundException(username + "not found");
}
}
三、拦截请求,xml基本配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd">
<!-- 不需要安全控制 -->
<http pattern="/login.html" security="none"></http>
<http pattern="/login_error.html" security="none"></http>
<!-- 页面的拦截规则 use-expressions:是否启动SpEL表达式 默认是true -->
<http use-expressions="false">
<!-- 1 当前用户必须有ROLE_USER的角色 才可以访问根目录及所属子目录的资源 -->
<intercept-url pattern="/**" access="ROLE_USER"/>
<!-- 2 使用SpEL表达式-->
<intercept-url pattern="/**" access="hasRole('ROLE_USER')"/>
<!-- 开启表单登陆功能,修改默认登录 -->
<form-login login-page="/login.html" default-target-url="/index.html" authentication-failure-url="/login_error.html"/>
<!-- 关闭CSRF跨站请求伪造 -->
<csrf disabled="true"/>
</http>
<!-- 认证管理器 -->
<authentication-manager>
<authentication-provider>
<user-service>
<!-- 定义用户名和某一角色 -->
<user name="user" password="123456" authorities="ROLE_USER"/>
<user name="admin" password="123456" authorities="ROLE_ADMIN"/>
</user-service>
</authentication-provider>
</authentication-manager>
</beans:beans>
更改配置:login-processing-url="/login2" 对应的表单提交一定是action="/login2" method="post"。username-parameter="user" 对应的表单提交应该是name="user",密码名也可以更改。
<headers>
<frame-options policy="SAMEORIGIN"/> //允许使用内置框架页。
</headers>
四、拦截请求Java的方式配置
@Configuration
@EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.loginPage("/login")
.and()
.logout()
.logoutSuccessUrl("/")
.and()
.rememberMe()
.tokenRepository(new InMemoryTokenRepositoryImpl())
.tokenValiditySeconds(2419200)//四周有效,单位秒
.key("spittrKey")
.and()
.httpBasic()
.realmName("Spittr")
.and()
.authorizeRequests()
.antMatchers("/").authenticated()
.antMatchers("/spitter/me").authenticated()
.antMatchers(HttpMethod.POST, "/spittles").authenticated()
.anyRequest().permitAll()
.and()
.requiresChannel()
.antMatches("/spitter/form").requiresSecure()//需要HTTPS
.and()
.csrf()
.disable();//禁用CSRF防护功能
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user").password("password").roles("USER");
}
}
五、项目中自定义认证类
5.1 定义认证类,添加角色列表,返回认证对象。
public class UserDetailsServiceImpl implements UserDetailsService {
private SellerService sellerService;
public void setSellerService(SellerService sellerService) {
this.sellerService = sellerService;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("经过了UserDetailsServiceImpl");
//构建角色列表
List<GrantedAuthority> grantAuths=new ArrayList();
grantAuths.add(new SimpleGrantedAuthority("ROLE_SELLER"));
//得到商家对象
TbSeller seller = sellerService.findOne(username);
if(seller!=null){
if(seller.getStatus().equals("1")){
return new User(username,seller.getPassword(),grantAuths);
}else{
return null;
}
}else{
return null;
}
}
}
5.2 在web的spring目录下创建spring-security.xml,使用dubbo调用服务方法。
<!-- 以下页面不被拦截 -->
<http pattern="/*.html" security="none"></http>
<http pattern="/css/**" security="none"></http>
<http pattern="/img/**" security="none"></http>
<http pattern="/js/**" security="none"></http>
<http pattern="/plugins/**" security="none"></http>
<http pattern="/seller/add.do" security="none"></http>
<!-- 页面拦截规则 -->
<http use-expressions="false">
<intercept-url pattern="/**" access="ROLE_SELLER" />
<form-login login-page="/shoplogin.html" default-target-url="/admin/index.html" authentication-failure-url="/shoplogin.html" always-use-default-target="true"/>
<csrf disabled="true"/>
<headers>
<frame-options policy="SAMEORIGIN"/>
</headers>
<logout/>
</http>
<!-- 认证管理器 -->
<authentication-manager>
<authentication-provider user-service-ref="userDetailService">
</authentication-provider>
</authentication-manager>
<!-- 认证类 -->
<beans:bean id="userDetailService" class="com.pinyougou.service.UserDetailsServiceImpl">
<beans:property name="sellerService" ref="sellerService"></bean:property>
</beans:bean>
<!-- 引用dubbo 服务 -->
<dubbo:application name="pinyougou-shop-web" />
<dubbo:registry address="zookeeper://192.168.25.129:2181"/>
<dubbo:reference id="sellerService" interface="com.pinyougou.sellergoods.service.SellerService" >
</dubbo:reference>
5.3登录页面的表单提交:οnclick="document:loginform.submit()" ,loginform为表单的id。<form id="loginform" action="/login" method="post" >
六、BCrypt(60位字段)加密算法
用户表的密码通常使用MD5(哈希算法,32字符串加密)等不可逆算法加密后存储,大数据有时可以破解。为防止彩虹表破解更会先使用一个特定的字符串(如域名)加密,然后再使用一个随机的salt(盐值)加密。 特定字符串是程序代码中固定的,salt是每个密码单独随机,一般给用户表加一个字段单独存储,比较麻烦。 BCrypt算法将salt随机并混入最终加密后的密码,验证时也无需单独提供之前的salt,从而无需单独处理salt问题。
同样的密码加密后结果不同,极大地增加了破解密码的难度。
@RequestMapping("/add")
public Result add(@RequestBody TbSeller seller){
//密码加密
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String password = passwordEncoder.encode(seller.getPassword());
seller.setPassword(password);
try {
sellerService.add(seller);
return new Result(true, "增加成功");
} catch (Exception e) {
e.printStackTrace();
return new Result(false, "增加失败");
}
}