启动方式
IDE启动
mvn spring-boot:run
mvn install 编译微jar包后,运行java -jar 项目的jar名
属性配置 Spring Boot 不单单从 application.properties 获取配置,所以我们可以在程序中多种设置配置属性。按照以下列表的优先级排列:
命令行参数
java:comp/env 里的 JNDI 属性
JVM 系统属性
操作系统环境变量
RandomValuePropertySource 属性类生成的 random.* 属性
应用以外的 application.properties(或 yml)文件
打包在应用内的 application.properties(或 yml)文件
在应用 @Configuration 配置类中,用 @PropertySource 注解声明的属性文件
SpringApplication.setDefaultProperties 声明的默认属性在.properties或.yml配置文件中配置。
yml配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 name: 刘备 age: 55 person: name: 刘备 age: 55 spring: profiles: active: dev
资源映射 1 2 3 4 5 6 7 8 9 10 11 @Configuration public class WebMvcConfiguration extends WebMvcConfigurerAdapter { @Override public void addResourceHandlers (ResourceHandlerRegistry registry) { registry .addResourceHandler("/image/**" ) .addResourceLocations("file:///" + Utils.getResPath()); super .addResourceHandlers(registry); } }
Controller
@Controller:处理http请求
@RestController:@ResponseBody+@Controller
@RequestMapping:配置url映射
@PathVariable:获取url中参数
@RequestParam:获取请求参数的值
@GetMapping
@PostMapping
@PutMapping
@DeleteMapping
注意:
@RequestMapping(“/hello/{id}) …(@PathVariable(“id”) Integer id) ——》这种方式的url直接在反斜杠后加参数即可访问
而如果使用传统URL(…/..?id=..),则需要使用@RequestParam注解。
GetMapping= RequestMapping+GET
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @RestController public class TestController { @Value("${name}") private String name; @Autowired private Person person; @GetMapping("/hello/{id}") public Person hello (@PathVariable("id") String hello) { return person; } }
表单验证 对某个字段添加限制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Min(value = 18, message = "未成年") private Integer age;@PostMapping("/user") public User addUser (@Valid User u, BindingResult result) { if (result.hasErrors()){ System.out.println(result.getFieldError().getDefaultMessage()); return null ; } User user = new User(); user.setAge(u.getAge()); user.setName(u.getName()); return userRepository.save(user); }
DI 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class DI () { private DependencyA a; @Autowired public DI (DependencyA a) { this .a = a; } private DependencyB b; @Autowired public void setDependencyB (DependencyB b) { this .b = b; } @Autowired private DependencyC c; }
构造器注入:当有十几个甚至更多对象需要注入时,你的构造函数的参数个数可能会长到无法想像。
field反射注入:如果不使用Spring框架,这个属性只能通过反射注入,根本不符合JavaBean规范。还有,当不是用Spring创建的对象时,还可能引起NullPointerException。并且,不能用final修饰这个属性。
setter方法注入:不能将属性设置为final。
如果注入的属性是必选的属性,则通过构造器注入。
如果注入的属性是可选的属性,则通过setter方法注入。
至于field注入,不建议使用。
AOP 是一种编程思想,不是语言特有的。
使用@Aspect注解将一个java类定义为切面类
使用@Pointcut定义一个切入点,可以是一个规则表达式,比如下例中某个package下的所有函数,也可以是一个注解等。
根据需要在切入点不同位置的切入内容
使用@Before在切入点开始处切入内容
使用@After在切入点结尾处切入内容
使用@AfterReturning在切入点return内容之后切入内容(可以用来对处理返回值做一些加工处理)
使用@Around在切入点前后切入内容,并自己控制何时执行切入点自身的内容
使用@AfterThrowing用来处理当切入内容部分抛出异常之后的处理逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @org .aspectj.lang.annotation.Aspect@Component public class Aspect { @Pointcut("execution(public * com.hearing.springdemo.controller.*.*(..))") public void log () { } @Before("log()") public void doBefore (JoinPoint joinPoint) { Object[] args = joinPoint.getArgs(); System.out.println(Arrays.toString(args)); System.out.println("doBefore..." ); } @AfterReturning(returning = "object", pointcut = "log()") public void doAfter (Object object) { System.out.println("doAfter..." ); } }
Listener 执行顺序:监听器、过滤器、拦截器。
分类
监听域对象自身的创建和销毁的事件监听器 ServletContextListener,HttpSessionListener,ServletRequestListener。
监听域对象中属性的增加和删除的事件监听器 ServletContextAttributeListener,HttpSessionAttributeListener,ServletRequestAttributeListener。
监听绑定到HttpSession域中的某个对象的状态的事件监听器 HttpSessionBindingListener:对象的绑定与解绑(将要绑定对象实现该接口) HttpSessionActivationListener:对象的钝化与活化(将要绑定对象实现该接口)
Session钝化机制:将服务器中不经常使用的Session对象暂时序列化到文件系统或数据库系统中,当被使用时反序列化到内存。
使用 @WebListener方式 主类添加@ServletComponentScan注解
1 2 3 4 5 6 7 8 9 10 11 12 13 @WebListener public class MyServletContextListener implements ServletContextListener { @Override public void contextInitialized (ServletContextEvent sce) { System.out.println(sce.getServletContext().getServletContextName()+" init" ); } @Override public void contextDestroyed (ServletContextEvent sce) { System.out.println(sce.getServletContext().getServletContextName()+" destroy" ); } }
ServletListenerRegistrationBean代码注册 1 2 3 4 5 6 7 8 9 10 @Configuration public class ListenerConfigure { @Bean public ServletListenerRegistrationBean<MyHttpSessionListener> serssionListenerBean () { ServletListenerRegistrationBean<MyHttpSessionListener> sessionListener = new ServletListenerRegistrationBean<MyHttpSessionListener>(new MyHttpSessionListener()); return sessionListener; } }
Filter 概述 与Servlet相似,过滤器是一些web应用程序组件,可以绑定到一个web应用程序中。但是与其他web应用程序组件不同的是,过滤器是”链”在容器的处理过程中的。这就意味着它们会在servlet处理器之前访问一个进入的请求,并且在外发响应信息返回到客户前访问这些响应信息。这种访问使得过滤器可以检查并修改请求和响应的内容。
chain.doFilter将请求转发给过滤器链下一个filter , 如果没有filter那就是你请求的资源。可通过配置CharacterEncodingFilter来解决请求乱码的问题。
步骤
实现Filter【javax.servlet.Filter】接口,实现Filter方法。
添加 @Configuration 注解,将自定义Filter加入过滤链;或Filter中添加@WebFilter注解,主类添加@ServletComponentScan注解
1 2 3 4 5 6 7 8 9 10 11 @Bean public FilterRegistrationBean testFilterRegistration () { FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setFilter(new TestFilter()); registration.addUrlPatterns("/*" ); registration.addInitParameter("paramName" , "paramValue" ); registration.setName("testFilter" ); registration.setOrder(1 ); return registration; }
或:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Order(1) @WebFilter(filterName = "testFilter1", urlPatterns = "/*") public class TestFilterFirst implements Filter { @Override public void init (FilterConfig filterConfig) throws ServletException { } @Override public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("TestFilter1" ); filterChain.doFilter(servletRequest,servletResponse); } @Override public void destroy () { } }
拦截器 概述 SpringBoot的拦截器只能拦截流经DispatcherServlet的请求,对于自定义的Servlet无法进行拦截。
SpringMVC中的拦截器有两种:HandlerInterceptor和WebMvcInterceptor。
拦截器类似于Servlet开发中的过滤器Filter,用于对处理器进行预处理和后处理。
日志记录:记录请求信息的日志,以便进行信息监控、信息统计等。
权限检查:如登录检测,如果没有直接返回到登录页面;
性能监控:可以通过拦截器在进入处理器之前记录开始时间,在处理完后记录结束时间,从而得到该请求的处理时间(如果有反向代理,如apache可以自动记录);
通用行为:读取cookie得到用户信息并将用户对象放入请求,从而方便后续流程使用,还有如提取Locale、Theme信息,解决请求乱码等,只要是多个处理器都需要的即可使用拦截器实现。
OpenSessionInView:如hibernate,在进入处理器打开Session,在完成后关闭Session。
拦截器是AOP的一种实现,底层通过动态代理模式完成。区别:
拦截器是基于Java的反射机制的,而过滤器是基于函数回调。
拦截器不依赖于servlet容器,而过滤器依赖于servlet容器。
拦截器只能对action请求起作用,而过滤器则可以对几乎所有的请求起作用。
拦截器可以访问action上下文、值栈里的对象,而过滤器不能。
在action的生命周期中,拦截器可以多次被调用,而过滤器只能在容器初始化时被调用一次。
实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 public class WebAppConfig extends WebMvcConfigurerAdapter { @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(new InterceptorConfig()).addPathPatterns("api/path/**" ).excludePathPatterns("api/path/login" ); } } public class InterceptorConfig implements HandlerInterceptor { private static final Logger log = LoggerFactory.getLogger(InterceptorConfig.class); @Override public boolean preHandle (HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception { log.info("---------------------开始进入请求地址拦截----------------------------" ); HttpSession session = httpServletRequest.getSession(); if (!StringUtils.isEmpty(session.getAttribute("userName" ))){ return true ; } else { PrintWriter printWriter = httpServletResponse.getWriter(); printWriter.write("{code:0,message:\"session is invalid,please login again!\"}" ); return false ; } } @Override public void postHandle (HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { log.info("--------------处理请求完成后视图渲染之前的处理操作---------------" ); } @Override public void afterCompletion (HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { log.info("---------------视图渲染之后的操作-------------------------0" ); } }
执行顺序
跨域 概述 同源策略[same origin policy]是浏览器的一个安全功能,不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源。 同源策略是浏览器安全的基石。
源[origin]就是协议、域名和端口号。若地址里面的协议、域名和端口号均相同则属于同源。
哪些操作不受同源策略限制:
页面中的链接,重定向以及表单提交是不会受到同源策略限制的;
跨域资源的引入是可以的。但是JS不能读写加载的内容。如嵌入到页面中的, , ,
跨域:
受前面所讲的浏览器同源策略的影响,不是同源的脚本不能操作其他源下面的对象。想要操作另一个源下的对象就需要跨域。 在同源策略的限制下,非同源的网站之间不能发送 AJAX 请求。
跨域方法:
降域:可以通过设置 document.damain=’a.com’,浏览器就会认为它们都是同一个源。想要实现以上任意两个页面之间的通信,两个页面必须都设置documen.damain=’a.com’。
JSONP跨域
CORS 跨域
H5中的新特性:Cross-Origin Resource Sharing(跨域资源共享)。通过它,开发者(主要指后端开发者)可以决定资源是否能被跨域访问。cors是一个w3c标准,它允许浏览器(目前ie8以下还不能被支持)像我们不同源的服务器发出xmlHttpRequest请求,我们可以继续使用ajax进行请求访问。
CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。
SpringBoot跨域 允许全部请求跨域许可:
1 2 3 4 5 6 7 8 @Configuration public class MyWebAppConfigurer extends WebMvcConfigurerAdapter { @Override public void addCorsMappings (CorsRegistry registry) { registry.addMapping("/**" ); } }
有针对性跨域许可:
1 2 3 4 5 6 7 8 9 10 11 @Configuration public class MyWebAppConfigurer extends WebMvcConfigurerAdapter { @Override public void addCorsMappings (CorsRegistry registry) { registry.addMapping("/api/**" ) .allowedOrigins("http://192.168.1.97" ) .allowedMethods("GET" , "POST" ) .allowCredentials(false ).maxAge(3600 ); } }
针对某个Controller跨域许可:
1 2 3 4 5 @CrossOrigin(origins = "http://192.168.1.97:8080", maxAge = 3600) @RequestMapping("rest_index") @RestController public class IndexController {}
统一异常处理 1 2 3 4 5 6 7 8 9 @ControllerAdvice public class MyErrorController { @ExceptionHandler(RuntimeException.class) public String error () { System.out.println("error......" ); return "error..." ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Bean public EmbeddedServletContainerCustomizer containerCustomizer () { return (container -> { ErrorPage error401Page = new ErrorPage(HttpStatus.UNAUTHORIZED, "/Login401" ); ErrorPage error403Page = new ErrorPage(HttpStatus.FORBIDDEN, "/Login403" ); ErrorPage error404Page = new ErrorPage(HttpStatus.NOT_FOUND, "/Login404" ); ErrorPage error500Page = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/Login500" ); container.addErrorPages(error401Page, error403Page, error404Page, error500Page); }); }
创建定时任务(@Scheduled)
在Spring Boot的主类中加入@EnableScheduling注解,启用定时任务的配置。
创建定时任务实现类。
1 2 3 4 5 6 7 8 9 @Component public class ScheduleTask { private SimpleDateFormat format = new SimpleDateFormat("HH-MM-SS" ); @Scheduled(fixedDelay = 1000) public void repeat () { System.out.println("now is " + format.format(System.currentTimeMillis())); } }
异步调用(@Async)
在Spring Boot的主程序中配置@EnableAsync,方法体上通过使用@Async注解就能简单的将原来的同步函数变为异步函数。此时主函数不会理会它们的执行情况。
异步回调:为了让doTaskOne、doTaskTwo、doTaskThree能正常结束,假设我们需要统计一下三个任务并发执行共耗时多少,这就需要等到上述三个函数都完成调动之后记录时间,并计算结果。我们需要使用Future<T>来返回异步调用的结果,就像如下方式改造doTaskOne函数(doAsync方法需要手动调用):
1 2 3 4 5 @Async public Future<String> doAsync () { System.out.println("doAsync..." ); return new AsyncResult<>("doAsync finished" ); }
邮件发送
添加依赖spring-boot-starter-mail
配置
Controller
1 2 3 4 5 spring: mail: host: smtp.163.com username: 123456 @163.com password: xxxx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Autowired JavaMailSender mailSender; @GetMapping("/send") public String sendEmail () { try { final MimeMessage mimeMessage = this .mailSender.createMimeMessage(); final MimeMessageHelper message = new MimeMessageHelper(mimeMessage); message.setFrom("123456@163.com" ); message.setTo("123456@qq.com" ); message.setSubject("测试邮件主题" ); message.setText("测试邮件内容.........." ); this .mailSender.send(mimeMessage); return "send successful" ; } catch (MessagingException e) { e.printStackTrace(); } return "send failed" ; }
整合Redis 单节点Redis
配置
1 2 3 redis: host: localhost port: 6379
RedisService类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Repository public class RedisService { @Autowired private StringRedisTemplate template; public void set (String key, String value) { ValueOperations<String, String> operations = template.opsForValue(); operations.set(key, value); } public String get (String key) { ValueOperations<String, String> operations = template.opsForValue(); if (operations.get(key) == null ){ set(key, "default" ); return null ; } return operations.get(key); } }
Redis数据类型
1 2 3 4 5 ValueOperations<String, String> operations = template.opsForValue(); SetOperations<String, String> operations = template.opsForSet(); HashOperations<String, String> operations = template.opsForHash(); ListOperations<String, String> operations = template.opsForList(); ZsetOperations<String, String> operations = template.opsForZSet();
RedisTemplate和StringRedisTemplate
RedisTemplate:
StringRedisTemplate:
Redis集群
配置文件
1 2 3 redis: cluster: nodes: localhost:6379,localhost:6380,localhost:6381
整合集群
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Configuration public class RedisConfig { @Value("${spring.redis.cluster.nodes}") private String clusterNodes; @Bean public JedisCluster getJedisCluster () { String[] nodes = clusterNodes.split("," ); Set<HostAndPort> nodeSet = new HashSet<>(); for (String node: nodes){ String[] hp = node.split(":" ); nodeSet.add(new HostAndPort(hp[0 ], Integer.parseInt(hp[1 ]))); } return new JedisCluster(nodeSet); } }
Jedis 添加依赖:compile group: ‘redis.clients’, name: ‘jedis’, version: ‘2.9.0’
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Configuration public class JedisConfig { private int port = 6379 ; private String host = "localhost" ; @Bean public JedisPoolConfig getJedisPoolConfig () { System.out.println("getJedisPoolConfig..." ); return new JedisPoolConfig(); } @Bean public JedisPool getJedisPool () { JedisPoolConfig jedisPoolConfig = getJedisPoolConfig(); JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port); System.out.println("getJedisPool..." ); return jedisPool; } } @Autowired private JedisPool jedisPool;@GetMapping("/jedis/{name}") public String testJedis (@PathVariable("name") String name) { jedisPool.getResource().set("name" , name); return jedisPool.getResource().get("name" ); }
Redisson 缓存 EhCache缓存
添加spring-boot-starter-cache依赖,主类添加@EnableCaching注解。
在数据访问接口中,添加缓存配置注解。
1 2 3 4 5 @CacheConfig(cacheNames = "users") public interface UserRepository extends JpaRepository <User , long > { @Cacheable User findByName (String name) ; }
@CacheConfig:主要用于配置该类中会用到的一些共用的缓存配置。在这里@CacheConfig(cacheNames = “users”):配置了该数据访问对象中返回的内容将存储于名为users的缓存对象中,我们也可以不使用该注解,直接通过@Cacheable自己配置缓存集的名字来定义。
@Cacheable:配置了findByName函数的返回值将被加入缓存。同时在查询时,会先从缓存中获取,若不存在才再发起对数据库的访问。该注解主要有下面几个参数:
value、cacheNames:两个等同的参数(cacheNames为Spring 4新增,作为value的别名),用于指定缓存存储的集合名。由于Spring 4中新增了@CacheConfig,因此在Spring 3中原本必须有的value属性,也成为非必需项了
key:缓存对象存储在Map集合中的key值,非必需,缺省按照函数的所有参数组合作为key值,若自己配置需使用SpEL表达式,比如:@Cacheable(key = “#p0”):使用函数第一个参数作为缓存的key值,更多关于SpEL表达式的详细内容可参考官方文档
condition:缓存对象的条件,非必需,也需使用SpEL表达式,只有满足表达式条件的内容才会被缓存,比如:@Cacheable(key = “#p0”, condition = “#p0.length() < 3”),表示只有当第一个参数的长度小于3的时候才会被缓存,若做此配置上面的AAA用户就不会被缓存,读者可自行实验尝试。
unless:另外一个缓存条件参数,非必需,需使用SpEL表达式。它不同于condition参数的地方在于它的判断时机,该条件是在函数被调用之后才做判断的,所以它可以通过对result进行判断。
keyGenerator:用于指定key生成器,非必需。若需要指定一个自定义的key生成器,我们需要去实现org.springframework.cache.interceptor.KeyGenerator接口,并使用该参数来指定。需要注意的是:该参数与key是互斥的
cacheManager:用于指定使用哪个缓存管理器,非必需。只有当有多个时才需要使用
cacheResolver:用于指定使用那个缓存解析器,非必需。需通过org.springframework.cache.interceptor.CacheResolver接口来实现自己的缓存解析器,并用该参数指定。
@CachePut:配置于函数上,能够根据参数定义条件来进行缓存,它与@Cacheable不同的是,它每次都会真是调用函数,所以主要用于数据新增和修改操作上。它的参数与@Cacheable类似,具体功能可参考上面对@Cacheable参数的解析
@CacheEvict:配置于函数上,通常用在删除方法上,用来从缓存中移除相应数据。除了同@Cacheable一样的参数之外,它还有下面两个参数:
allEntries:非必需,默认为false。当为true时,会移除所有数据
beforeInvocation:非必需,默认为false,会在调用方法之后移除数据。当为true时,会在调用方法之前移除数据。
在Spring Boot中通过@EnableCaching注解自动化配置合适的缓存管理器(CacheManager),Spring Boot根据下面的顺序去侦测缓存提供者:
Generic
JCache (JSR-107)
EhCache 2.x
Hazelcast
Infinispan
Redis
Guava
Simple
除了按顺序侦测外,我们也可以通过配置属性spring.cache.type来强制指定。Spring boot默认使用的是SimpleCacheConfiguration,即使用ConcurrentMapCacheManager来实现缓存。下面以常用的EhCache为例,看看如何配置来使用EhCache进行缓存管理。
在Spring Boot中开启EhCache非常简单,只需要在工程中加入ehcache.xml配置文件并在pom.xml中增加ehcache依赖,框架只要发现该文件,就会创建EhCache的缓存管理器。在src/main/resources目录下创建:ehcache.xml
1 2 3 4 <?xml version="1.0" encoding="UTF-8"?> <ehcache > <cache name ="people" maxElementsInMemory ="1000" /> </ehcache >
对于EhCache的配置文件也可以通过application.properties文件中使用spring.cache.ehcache.config属性来指定,比如:
spring.cache.ehcache.config=classpath:config/another-config.xml
Redis缓存 虽然EhCache已经能够适用很多应用场景,但是由于EhCache是进程内的缓存框架,在集群模式下时,各应用服务器之间的缓存都是独立的,因此在不同服务器的进程间会存在缓存不一致的情况。即使EhCache提供了集群环境下的缓存同步策略,但是同步依然需要一定的时间,短暂的缓存不一致依然存在。在一些要求高一致性(任何数据变化都能及时的被查询到)的系统和应用中,就不能再使用EhCache来解决了,这个时候使用集中式缓存是个不错的选择,因此本文将介绍如何在Spring Boot的缓存支持中使用Redis进行数据缓存。
添加spring-boot-starter-redis依赖,主类开启@EnableCaching注解。
在配置文件中添加对redis的配置。
在数据访问接口中,添加缓存配置注解。
生命周期控制 由于EhCache是进程内的缓存框架,第一次通过select查询出的结果被加入到EhCache缓存中,第二次查询从EhCache取出的对象与第一次查询对象实际上是同一个对象,因此我们在更新age的时候,实际已经更新了EhCache中的缓存对象(是一个对象引用) 。而Redis的缓存独立存在于我们的Spring应用之外,我们对数据库中数据做了更新操作之后,没有通知Redis去更新相应的内容,因此我们取到了缓存中未修改的数据,导致了数据库与缓存中数据的不一致。
因此我们在使用缓存的时候,要注意缓存的生命周期。在更新age的时候,通过@CachePut来让数据更新操作同步到缓存中,就像下面这样(使用Redis时User需实现Serializable接口):
1 2 3 4 5 6 7 8 @CacheConfig(cacheNames = "users") public interface UserRepository extends JpaRepository <User , long > { @Cacheable(key = "#p0") User findByName (String name) ; @CachePut(key = "#p0.name") User save (User user) ; }
整合Spring-Session 虽然Session保存在服务器,对客户端是透明的,它的正常运行仍然需要客户端浏览器的支持。这是因为Session需要使用Cookie作为识别标志。HTTP协议是无状态的,Session不能依据HTTP连接来判断是否为同一客户,因此服务器向客户端浏览器发送一个名为JSESSIONID的Cookie,它的值为该Session的id(也就是HttpSession.getId()的返回值)。Session依据该Cookie来识别是否为同一用户。
轻易把session存储到第三方存储容器,框架提供了redis、jvm的map、mongo、gemfire、hazelcast、jdbc等多种存储session的容器的方式。
同一个浏览器同一个网站,支持多个session问题。
Restful API,不依赖于cookie。可通过header来传递jessionID
WebSocket和spring-session结合,同步生命周期管理。
引入依赖:spring-session-data-redis和spring-boot-starter-data-redis
配置redis连接后在主类上添加@EnableRedisHttpSession注解
整合SpringData-Jpa 核心原理是AOP.
SpringData Repository分类
Repository: 仅仅是一个标识,表明任何继承它的均为仓库接口类
CrudRepository: 继承 Repository,实现了一组 CRUD 相关的方法
PagingAndSortingRepository: 继承 CrudRepository,实现了一组分页排序相关的方法
JpaRepository: 继承 PagingAndSortingRepository,实现一组 JPA 规范相关的方法
自定义的 XxxxRepository 需要继承 JpaRepository,这样的 XxxxRepository 接口就具备了通用的数据访问控制层的能力。
JpaSpecificationExecutor: 不属于Repository体系,实现一组 JPA Criteria 查询相关的方法
方法定义规范
简单条件查询: 查询某一个实体类或者集合
按照 Spring Data 的规范,查询方法以 find | read | get 开头,涉及条件查询时,条件的属性用条件关键字连接,要注意的是:条件属性以首字母大写。
1 2 3 4 class User { private String firstName ; private String lastName; }
使用And条件连接时,应这样写:
findByLastNameAndFirstName(String lastName,String firstName); 条件的属性名称与个数要与参数的位置与个数一一对应
@Query和@Modifying 注解在Repository方法中,实现查询和更改插入操作.
使用
配置文件中:
1 2 3 4 5 6 7 8 9 10 spring: datasource: username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/spring?autoReconnect=true&useSSL=false jpa: hibernate: ddl-auto: update show-sql: true
此时可连接到数据库(需要mysql-connector-java和spring-boot-starter-data-jpa依赖)。
JavaBean:
1 2 3 4 5 6 7 8 9 10 @Entity public class User { @Id @GeneratedValue private Integer id; private String name; private Integer age; public User () { } setter+getter }
可自动生成数据表(需要存在空构造函数)。
定义接口:JpaRepository
1 public interface UserRepository extends JpaRepository <User , Integer > { }
使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Autowired private UserRepository userRepository;@GetMapping("/user") public List<User> getUser () { return userRepository.findAll(); } @PostMapping("/user") public User addUser (User u) { User user = new User(); user.setAge(u.getAge()); user.setName(u.getName()); return userRepository.save(user); }
整合MyBatis MyBatis
MyBatis正向工程:
导包
Spring全局配置文件
写接口,接口不用实现(代理),与sql映射文件中的一句sql对应
写sql映射文件
获取使用
MyBatis逆向工程
导包
Spring全局配置文件
建立数据库表
逆向工程配置文件
代码中运行该配置文件,生成接口和sql映射文件
获取使用
使用
导入Mybatis的依赖:mybatis-spring-boot-starter,mybatis-generator-core,mysql-connector-java
在properties或yml文件中
1 2 3 4 5 6 7 8 9 10 mybatis: mapper-locations: classpath:mapper/UserMapper.xml spring: datasource: username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/spring?autoReconnect=true&useSSL=false&nullCatalogMeansCurrent=true&useUnicode=true&characterEncoding=UTF-8
nullCatalogMeansCurrent配置是为了防止在Connector JDBC的8.x版中,自动生成WithBLOBS的问题.
逆向工程生成Mapper
配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd" > <generatorConfiguration > <context id ="DB2Tables" targetRuntime ="MyBatis3" > <commentGenerator > <property name ="suppressAllComments" value ="true" /> </commentGenerator > <jdbcConnection driverClass ="com.mysql.cj.jdbc.Driver" connectionURL ="jdbc:mysql:///spring?autoReconnect=true& useSSL=false& nullCatalogMeansCurrent=true& useUnicode=true& characterEncoding=UTF-8" userId ="root" password ="123456" > </jdbcConnection > <javaModelGenerator targetPackage ="com.hearing.springdemo.bean" targetProject ="./src/main/java" > <property name ="enableSubPackages" value ="true" /> <property name ="trimStrings" value ="true" /> </javaModelGenerator > <sqlMapGenerator targetPackage ="mapper" targetProject ="./src/main/resources" > <property name ="enableSubPackages" value ="true" /> </sqlMapGenerator > <javaClientGenerator type ="XMLMAPPER" targetPackage ="com.hearing.springdemo.dao" targetProject ="./src/main/java" > <property name ="enableSubPackages" value ="true" /> </javaClientGenerator > <table tableName ="user" domainObjectName ="User" > </table > </context > </generatorConfiguration >
run该配置文件:
1 2 3 4 5 6 7 8 List<String> warnings = new ArrayList<String>(); boolean overwrite = true ;File configFile = new File("mybatis.xml" ); ConfigurationParser cp = new ConfigurationParser(warnings); Configuration config = cp.parseConfiguration(configFile); DefaultShellCallback callback = new DefaultShellCallback(overwrite); MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings); myBatisGenerator.generate(null );
在Application.java中使用注解配置Mapper接口:@MapperScan(“com.example.demo.dao”)
相关操作 插入记录返回主键值 1 2 3 4 <insert id ="insertAndGetId" useGeneratedKeys ="true" keyProperty ="userId" parameterType ="com.hearing.mybatis.User" > insert into user(userName,password,comment) values(#{userName},#{password},#{comment}) </insert >
useGeneratedKeys=”true” 表示给主键设置自增长
keyProperty=”userId” 表示将自增长后的Id赋值给实体类中的userId字段。
parameterType=”com.hearing.mybatis.User” 这个属性指向传递的参数实体类
这里提醒下,<insert></insert>
中没有resultType属性,不要乱加。
实体类中uerId 要有getter() and setter(); 方法
数据库支持自增主键
原理
这是一个和我们平时使用不同的方式, 但如果细心观察,会发现, 实际上在 Spring 和 Mybatis 整合的框架中也是这么使用的, 只是 Spring 的 IOC 机制帮助我们屏蔽了创建对象的过程而已.SqlSession 的 getMapper 方法获取了一个代理对象, 调用代理对象的 selectById 方法 获取返回值.
SqlSessionFactory 该类的作用是创建 SqlSession, 该类使用了工厂模式, 每次应用程序访问数据库, 我们就要通过 SqlSessionFactory 创建 SqlSession, 所以SqlSessionFactory 和整个 Mybatis 的生命周期是相同的. 这也告诉我们不能创建多个同一个数据的 SqlSessionFactory, 如果创建多个, 会消耗尽数据库的连接资源, 导致服务器夯机. 应当使用单例模式. 避免过多的连接被消耗, 也方便管理.
SqlSession 相当于一个会话, 每次访问数据库都需要这样一个会话, 现在几乎所有的连接都是使用的连接池技术, 用完后直接归还而不会像 Session 一样销毁. 注意:他是一个线程不安全的对象, 在设计多线程的时候我们需要特别的当心, 操作数据库需要注意其隔离级别, 数据库锁等高级特性, 此外, 每次创建的 SqlSession 都必须及时关闭它, 它长期存在就会使数据库连接池的活动资源减少,对系统性能的影响很大, 一般在 finally 块中将其关闭. 还有, SqlSession 存活于一个应用的请求和操作,可以执行多条 Sql, 保证事务的一致性.
Mapper 映射器,Mapper 是一个接口, 没有任何实现类, 他的作用是发送 SQL, 然后返回我们需要的结果. 或者执行 SQL 从而更改数据库的数据, 因此它应该在 SqlSession 的事务方法之内, 在 Spring 管理的 Bean 中, Mapper 是单例的。
事务管理 在相应方法上使用@Transactional注解即可。
当我们项目较大较复杂时(比如,有多个数据源等),这时候需要在声明事务时,指定不同的事务管理器。在声明事务时,只需要通过value属性指定配置的事务管理器名即可,例如@Transactional(value=”transactionManagerPrimary”)。
除了指定不同的事务管理器之后,还能对事务进行隔离级别和传播行为的控制。
隔离级别 org.springframework.transaction.annotation.Isolation枚举类中定义了五个表示隔离级别的值:
DEFAULT:这是默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是:READ_COMMITTED。
READ_UNCOMMITTED(未提交读):该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读和不可重复读,因此很少使用该隔离级别。
READ_COMMITTED(已提交读):该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值。
REPEATABLE_READ(可重复读):该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。即使在多次查询之间有新增的数据满足该查询,这些新增的记录也会被忽略。该级别可以防止脏读和不可重复读。
SERIALIZABLE(可序列化):所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
指定方法:通过使用isolation属性设置,例如:@Transactional(isolation = Isolation.DEFAULT)
传播行为 所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。如果在Service层的一个方法中,除了调用了Dao层的方法之外,还调用了本类的其他的Service方法,即会产生事务的传播行为。
org.springframework.transaction.annotation.Propagation枚举类中定义了6个表示传播行为的枚举值:
REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。
NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于REQUIRED。
指定方法:通过使用propagation属性设置,例如:@Transactional(propagation = Propagation.REQUIRED)
整合ElasticSearch JWT 概述 Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
基于session认证所显露的问题:
Session: 每个用户经过应用认证之后,应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。
扩展性: 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。
CSRF: 因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。
基于token的鉴权机制:
基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。
流程:
用户使用用户名密码来请求服务器
服务器进行验证用户的信息
服务器通过验证发送给用户一个token
客户端存储token,并在每次请求时附送上这个token值
服务端验证token值,并返回数据
这个token必须要在每次请求时传递给服务端,它应该保存在请求头里,另外,服务端要支持CORS(跨来源资源共享)策略,一般我们在服务端这么做就可以了Access-Control-Allow-Origin: *。
构成 JWT是由三段信息构成的,将这三段信息文本用”.”链接一起就构成了Jwt字符串。
jwt的头部承载两部分信息:
声明类型,这里是jwt
声明加密的算法 通常直接使用 HMAC SHA256
1 2 3 4 { "typ" : "JWT" , "alg" : "HS256" }
然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
payload 载荷就是存放有效信息的地方,这些有效信息包含三个部分:
标准中注册的声明
公共的声明
私有的声明
标准中注册的声明 (建议但不强制使用):
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
公共的声明 :
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有的声明 :
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
1 2 3 4 5 { "sub" : "1234567890" , "name" : "John Doe" , "admin" : true }
然后将其进行base64加密,得到Jwt的第二部分:eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
signature 签证信息由三部分组成:
header (base64后的)
payload (base64后的)
secret
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
将这三部分用.连接成一个完整的字符串,构成了最终的jwt: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
使用 依赖 1 compile group: 'io.jsonwebtoken' , name: 'jjwt' , version: '0.9.0'
配置 1 2 3 4 token: secret: hearing-secret expiration: 600 header: TokenHeader
JWT工具类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class Md5Util { public static String getMD5 (String inStr) { MessageDigest md5 = null ; try { md5 = MessageDigest.getInstance("MD5" ); } catch (Exception e) { e.printStackTrace(); return "" ; } char [] charArray = inStr.toCharArray(); byte [] byteArray = new byte [charArray.length]; for (int i = 0 ; i < charArray.length; i++) byteArray[i] = (byte ) charArray[i]; byte [] md5Bytes = md5.digest(byteArray); StringBuffer hexValue = new StringBuffer(); for (int i = 0 ; i < md5Bytes.length; i++) { int val = ((int ) md5Bytes[i]) & 0xff ; if (val < 16 ) hexValue.append("0" ); hexValue.append(Integer.toHexString(val)); } return hexValue.toString(); } }
1 2 3 4 5 6 public interface TokenDetail { String getUsername () ; String getPassword () ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class TokenDetailImpl implements TokenDetail { private final String username; private final String password; public TokenDetailImpl (String username, String password) { this .username = username; this .password = password; } @Override public String getUsername () { return this .username; } @Override public String getPassword () { return this .password; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 @Component public class TokenUtils { private final Logger logger = Logger.getLogger(this .getClass()); @Value("${token.secret}") private String secret; @Value("${token.expiration}") private Long expiration; public String generateToken (TokenDetail tokenDetail) { Map<String, Object> claims = new HashMap<>(); claims.put("sub" , tokenDetail.getUsername()); claims.put("aud" , tokenDetail.getPassword()); claims.put("created" , this .generateCurrentDate()); return this .generateToken(claims); } private String generateToken (Map<String, Object> claims) { try { return Jwts.builder() .setClaims(claims) .setExpiration(this .generateExpirationDate()) .signWith(SignatureAlgorithm.HS512, this .secret.getBytes("UTF-8" )) .compact(); } catch (UnsupportedEncodingException ex) { logger.warn(ex.getMessage()); return Jwts.builder() .setClaims(claims) .setExpiration(this .generateExpirationDate()) .signWith(SignatureAlgorithm.HS512, this .secret) .compact(); } } private Date generateExpirationDate () { return new Date(System.currentTimeMillis() + this .expiration * 1000 ); } private Date generateCurrentDate () { return new Date(System.currentTimeMillis()); } public String getUsernameFromToken (String token) { String username; try { final Claims claims = this .getClaimsFromToken(token); username = claims.getSubject(); } catch (Exception e) { username = null ; } return username; } public String getPasswordFromToken (String token) { String password; try { final Claims claims = this .getClaimsFromToken(token); password = claims.getAudience(); } catch (Exception e) { password = null ; } return password; } private Claims getClaimsFromToken (String token) { Claims claims; try { claims = Jwts.parser() .setSigningKey(this .secret.getBytes("UTF-8" )) .parseClaimsJws(token) .getBody(); } catch (Exception e) { claims = null ; } return claims; } public Boolean validateToken (String token) { final String username = this .getUsernameFromToken(token); final String password = this .getPasswordFromToken(token); final Date created = this .getCreatedDateFromToken(token); return (!this .isTokenExpired(token)); } private Date getCreatedDateFromToken (String token) { Date created; try { final Claims claims = this .getClaimsFromToken(token); created = new Date((Long) claims.get("created" )); } catch (Exception e) { created = null ; } return created; } private Date getExpirationDateFromToken (String token) { Date expiration; try { final Claims claims = this .getClaimsFromToken(token); expiration = claims.getExpiration(); } catch (Exception e) { expiration = null ; } return expiration; } private Boolean isTokenExpired (String token) { final Date expiration = this .getExpirationDateFromToken(token); if (expiration == null ) { return true ; } return expiration.before(this .generateCurrentDate()); } private Boolean isCreatedBeforeLastPasswordReset (Date created, Date lastPasswordReset) { return (lastPasswordReset != null && created.before(lastPasswordReset)); } }
Filter 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 @Component public class LoginFilter implements Filter { @Value("${token.header}") private String tokenHeader; @Autowired private TokenUtils tokenUtils; private HttpSessionRequestCache requestCache; @Override public void init (FilterConfig filterConfig) throws ServletException { requestCache = new HttpSessionRequestCache(); } @Override public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("doFilter..." ); HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; String authToken = request.getHeader(this .tokenHeader); if (authToken != null ) { String username = this .tokenUtils.getUsernameFromToken(authToken); String password = this .tokenUtils.getPasswordFromToken(authToken); if (username != null && password != null && this .tokenUtils.validateToken(authToken)) { System.out.println("validate token!" ); filterChain.doFilter(servletRequest, servletResponse); } else { requestCache.saveRequest(request, response); response.sendRedirect("/login" ); } } else { requestCache.saveRequest(request, response); response.sendRedirect("/login" ); } } @Override public void destroy () { } }
SpringSecurity 认证
用户使用用户名和密码进行登录。
Spring Security 将获取到的用户名和密码封装成一个实现了 Authentication 接口的 UsernamePasswordAuthenticationToken。
将上述产生的 token 对象传递给 AuthenticationManager 进行登录认证。
AuthenticationManager 认证成功后将会返回一个封装了用户权限等信息的 Authentication 对象。
通过调用 SecurityContextHolder.getContext().setAuthentication(…) 将 AuthenticationManager 返回的 Authentication 对象赋予给当前的 SecurityContext。
如果用户直接访问一个受保护的资源,那么认证过程将如下:
引导用户进行登录,通常是重定向到一个基于 form 表单进行登录的页面,具体视配置而定。
用户输入用户名和密码后请求认证,后台还是会像上节描述的那样获取用户名和密码封装成一个 UsernamePasswordAuthenticationToken 对象,然后把它传递给 AuthenticationManager 进行认证。
如果认证失败将继续执行步骤 1,如果认证成功则会保存返回的 Authentication 到 SecurityContext,然后默认会将用户重定向到之前访问的页面。
用户登录认证成功后再次访问之前受保护的资源时就会对用户进行权限鉴定,如不存在对应的访问权限,则会返回 403 错误码。
WebSecurityConfigurerAdapter是SpringSecurity的配置类,通过@Configuration和@EnableWebSecurity注解即可配置.
Authentication Authentication 是一个接口,用来表示用户认证信息的,在用户登录认证之前相关信息会封装为一个 Authentication 具体实现类的对象,在登录认证成功之后又会生成一个信息更全面,包含用户权限等信息的 Authentication 对象,然后把它保存在 SecurityContextHolder 所持有的 SecurityContext 中,供后续的程序进行调用,如访问权限的鉴定等。
UserDetailsService 读取登录用户信息、权限
AbstractAuthenticationProcessingFilter 处理登录(UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter)
AuthenticationManager 和 AuthenticationProvider
Authorization 权限认证,权限认证中有四个重要的类:
UserDetailsService 读取登录用户信息、权限
AbstractSecurityInterceptor 这个类是用来继承的,还要实现servler的Filter,处理鉴权(FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter)
FilterInvocationSecurityMetadataSource 读取url资源
AccessDecisionManager 控制访问权限
SecurityContextHolder SecurityContextHolder 是用来保存 SecurityContext 的。SecurityContext 中含有当前正在访问系统的用户的详细信息。默认情况下,SecurityContextHolder 将使用 ThreadLocal 来保存 SecurityContext,这也就意味着在处于同一线程中的方法中我们可以从 ThreadLocal 中获取到当前的 SecurityContext。因为线程池的原因,如果我们每次在请求完成后都将 ThreadLocal 进行清除的话,那么我们把 SecurityContext 存放在 ThreadLocal 中还是比较安全的。这些工作 Spring Security 已经自动为我们做了,即在每一次 request 结束后都将清除当前线程的 ThreadLocal。
在程序的任何地方,通过如下方式可以获取到当前用户的用户名:
1 2 3 4 5 6 7 8 9 10 public String getCurrentUsername () { Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (principal instanceof UserDetails) { return ((UserDetails) principal).getUsername(); } if (principal instanceof Principal) { return ((Principal) principal).getName(); } return String.valueOf(principal); }
关于上述代码其实 Spring Security 在 Authentication 中的实现类中已经为我们做了相关实现,所以获取当前用户的用户名最简单的方式应当如下。
1 2 3 public String getCurrentUsername () { return SecurityContextHolder.getContext().getAuthentication().getName(); }
此外,调用 SecurityContextHolder.getContext() 获取 SecurityContext 时,如果对应的 SecurityContext 不存在,则 Spring Security 将为我们建立一个空的 SecurityContext 并进行返回。
AuthenticationManager 和 AuthenticationProvider AuthenticationManager 是一个用来处理认证(Authentication)请求的接口。在其中只定义了一个方法 authenticate(),该方法只接收一个代表认证请求的 Authentication 对象作为参数,如果认证成功,则会返回一个封装了当前用户权限等信息的 Authentication 对象进行返回。
1 Authentication authenticate (Authentication authentication) throws AuthenticationException ;
一个 AuthenticationManager 认证管理者可能会在 authenticate()方法中做下面三件事中的任意一个:
如果认证成功,返回 Authentication (通常它的authenticated属性为true authenticated=true) .
如果认证失败,抛出 AuthenticationException 异常.
如果无法判断,返回 null .
在 Spring Security 中,AuthenticationManager 的默认实现是 ProviderManager,而且它不直接自己处理认证请求,而是委托给其所配置的 AuthenticationProvider 列表,然后会依次使用每一个 AuthenticationProvider 进行认证,如果有一个 AuthenticationProvider 认证后的结果不为 null,则表示该 AuthenticationProvider 已经认证成功,之后的 AuthenticationProvider 将不再继续认证。然后直接以该 AuthenticationProvider 的认证结果作为 ProviderManager 的认证结果。如果所有的 AuthenticationProvider 的认证结果都为 null,则表示认证失败,将抛出一个 ProviderNotFoundException。校验认证请求最常用的方法是根据请求的用户名加载对应的 UserDetails,然后比对 UserDetails 的密码与认证请求的密码是否一致,一致则表示认证通过。Spring Security 内部的 DaoAuthenticationProvider 就是使用的这种方式。其内部使用 UserDetailsService 来负责加载 UserDetails,在认证成功以后会使用加载的 UserDetails 来封装要返回的 Authentication 对象,加载的 UserDetails 对象是包含用户权限等信息的。认证成功返回的 Authentication 对象将会保存在当前的 SecurityContext 中。
UserDetailsService 通过 Authentication.getPrincipal() 的返回类型是 Object,但很多情况下其返回的其实是一个 UserDetails 的实例。UserDetails 是 Spring Security 中一个核心的接口。其中定义了一些可以获取用户名、密码、权限等与认证相关的信息的方法。Spring Security 内部使用的 UserDetails 实现类大都是内置的 User 类,我们如果要使用 UserDetails 时也可以直接使用该类。在 Spring Security 内部很多地方需要使用用户信息的时候基本上都是使用的 UserDetails,比如在登录认证的时候。登录认证的时候 Spring Security 会通过 UserDetailsService 的 loadUserByUsername() 方法获取对应的 UserDetails 进行认证,认证通过后会将该 UserDetails 赋给认证通过的 Authentication 的 principal,然后再把该 Authentication 存入到 SecurityContext 中。之后如果需要使用用户信息的时候就是通过 SecurityContextHolder 获取存放在 SecurityContext 中的 Authentication 的 principal。
UserDetailsService 也是一个接口,我们需要实现自己的 UserDetailsService 来加载我们自定义的 UserDetails 信息。然后把它指定给 AuthenticationProvider 即可。
PasswordEncoder Spring Security 提供了BCryptPasswordEncoder类,实现Spring的PasswordEncoder接口使用BCrypt强哈希方法来加密密码。浅谈使用springsecurity中的BCryptPasswordEncoder方法对密码进行加密(encode)与密码匹配(matches).
AuthenticationSuccessHandler和AuthenticationFailureHandler 通过 .successHandler(loginSuccessHandler)和.failureHandler(loginFailedHandler)设置
在 request 之间共享 SecurityContext SecurityContext 是存放在 ThreadLocal 中的,而且在每次权限鉴定的时候都是从 ThreadLocal 中获取 SecurityContext 中对应的 Authentication 所拥有的权限,并且不同的 request 是不同的线程,那么为什么每次都可以从 ThreadLocal 中获取到当前用户对应的 SecurityContext 呢?
在 Web 应用中这是通过 SecurityContextPersistentFilter 实现的,默认情况下其会在每次请求开始的时候从 session 中获取 SecurityContext,然后把它设置给 SecurityContextHolder,在请求结束后又会将 SecurityContextHolder 所持有的 SecurityContext 保存在 session 中,并且清除 SecurityContextHolder 所持有的 SecurityContext。这样当我们第一次访问系统的时候,SecurityContextHolder 所持有的 SecurityContext 肯定是空的,待我们登录成功后,SecurityContextHolder 所持有的 SecurityContext 就不是空的了,且包含有认证成功的 Authentication 对象,待请求结束后我们就会将 SecurityContext 存在 session 中,等到下次请求的时候就可以从 session 中获取到该 SecurityContext 并把它赋予给 SecurityContextHolder 了,由于 SecurityContextHolder 已经持有认证过的 Authentication 对象了,所以下次访问的时候也就不再需要进行登录认证了。
CachingUserDetailsService Spring Security 提供了一个实现了可以缓存 UserDetails 的 UserDetailsService 实现类 CachingUserDetailsService。该类的构造接收一个用于真正加载 UserDetails 的 UserDetailsService 实现类。当需要加载 UserDetails 时,其首先会从缓存中获取,如果缓存中没有对应的 UserDetails 存在,则使用持有的 UserDetailsService 实现类进行加载,然后将加载后的结果存放在缓存中。UserDetails 与缓存的交互是通过 UserCache 接口来实现的。CachingUserDetailsService 默认拥有 UserCache 的一个空实现引用,NullUserCache。以下是 CachingUserDetailsService 的类定义。
当缓存中不存在对应的 UserDetails 时将使用引用的 UserDetailsService 类型的 delegate 进行加载。加载后再把它存放到 Cache 中并进行返回。除了 NullUserCache 之外,Spring Security 还提供了一个基于 Ehcache 的 UserCache 实现类 EhCacheBasedUserCache。
GrantedAuthority Authentication 的 getAuthorities() 可以返回当前 Authentication 对象拥有的权限,即当前用户拥有的权限。其返回值是一个 GrantedAuthority 类型的数组,每一个 GrantedAuthority 对象代表赋予给当前用户的一种权限。GrantedAuthority 是一个接口,其通常是通过 UserDetailsService 进行加载,然后赋予给 UserDetails 的。
GrantedAuthority 中只定义了一个 getAuthority() 方法,该方法返回一个字符串,表示对应权限的字符串表示,如果对应权限不能用字符串表示,则应当返回 null。
Spring Security 针对 GrantedAuthority 有一个简单实现 SimpleGrantedAuthority。该类只是简单的接收一个表示权限的字符串。Spring Security 内部的所有 AuthenticationProvider 都是使用 SimpleGrantedAuthority 来封装 Authentication 对象。
AccessDecisionManager 有三个方法:
public void decide(Authentication authentication, Object object, Collection configAttributes):它是判定是否拥有权限的决策方法.
authentication 是UserService中循环添加到 GrantedAuthority 对象中的权限信息集合.
object 包含客户端发起的请求的requset信息,可转换为 HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
configAttributes 为InvocationSecurityMetadataSource的getAttributes(Object object)这个方法返回的结果,此方法是为了判定用户请求的url 是否在权限表中,如果在权限表中,则返回给 decide 方法,用来判定用户是否有此权限。如果不在权限表中则放行。
public boolean supports(ConfigAttribute attribute):return true;
public boolean supports(Class<?> clazz):return true;
public Collection getAttributes(Object object):object 中包含用户请求的request 信息,返回url对应的权限表
AbstractSecurityInterceptor 调用FilterInvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取url对应的所有权限,再调用MyAccessDecisionManager的decide方法来校验用户的权限是否足够.
Filter 顺序 Spring Security 已经定义了一些 Filter,不管实际应用中用到了哪些,它们应当保持如下顺序:
ChannelProcessingFilter,如果你访问的 channel 错了,那首先就会在 channel 之间进行跳转,如 http 变为 https。
SecurityContextPersistenceFilter,这样的话在一开始进行 request 的时候就可以在 SecurityContextHolder 中建立一个 SecurityContext,然后在请求结束的时候,任何对 SecurityContext 的改变都可以被 copy 到 HttpSession。
ConcurrentSessionFilter,因为它需要使用 SecurityContextHolder 的功能,而且更新对应 session 的最后更新时间,以及通过 SessionRegistry 获取当前的 SessionInformation 以检查当前的 session 是否已经过期,过期则会调用 LogoutHandler。
认证处理机制,如 UsernamePasswordAuthenticationFilter,CasAuthenticationFilter,BasicAuthenticationFilter 等,以至于 SecurityContextHolder 可以被更新为包含一个有效的 Authentication 请求。
SecurityContextHolderAwareRequestFilter,它将会把 HttpServletRequest 封装成一个继承自 HttpServletRequestWrapper 的 SecurityContextHolderAwareRequestWrapper,同时使用 SecurityContext 实现了 HttpServletRequest 中与安全相关的方法。
JaasApiIntegrationFilter,如果 SecurityContextHolder 中拥有的 Authentication 是一个 JaasAuthenticationToken,那么该 Filter 将使用包含在 JaasAuthenticationToken 中的 Subject 继续执行 FilterChain。
RememberMeAuthenticationFilter,如果之前的认证处理机制没有更新 SecurityContextHolder,并且用户请求包含了一个 Remember-Me 对应的 cookie,那么一个对应的 Authentication 将会设给 SecurityContextHolder。
AnonymousAuthenticationFilter,如果之前的认证机制都没有更新 SecurityContextHolder 拥有的 Authentication,那么一个 AnonymousAuthenticationToken 将会设给 SecurityContextHolder。
ExceptionTransactionFilter,用于处理在 FilterChain 范围内抛出的 AccessDeniedException 和 AuthenticationException,并把它们转换为对应的 Http 错误码返回或者对应的页面。
FilterSecurityInterceptor,保护 Web URI,并且在访问被拒绝时抛出异常。
别名
类名称
Namespace Element or Attribute
CHANNEL_FILTER
ChannelProcessingFilter
http/intercept-url@requires-channel
SECURITY_CONTEXT_FILTER
SecurityContextPersistenceFilter
http
CONCURRENT_SESSION_FILTER
ConcurrentSessionFilter
session-management/concurrency-control
HEADERS_FILTER
HeaderWriterFilter
http/headers
CSRF_FILTER
CsrfFilter
http/csrf
LOGOUT_FILTER
LogoutFilter
http/logout
X509_FILTER
X509AuthenticationFilter
http/x509
PRE_AUTH_FILTER
AbstractPreAuthenticatedProcessingFilter(Subclasses)
N/A
CAS_FILTER
CasAuthenticationFilter
N/A
FORM_LOGIN_FILTER
UsernamePasswordAuthenticationFilter
http/form-login
BASIC_AUTH_FILTER
BasicAuthenticationFilter
http/http-basic
SERVLET_API_SUPPORT_FILTER
SecurityContextHolderAwareRequestFilter
http/@servlet-api-provision
JAAS_API_SUPPORT_FILTER
JaasApiIntegrationFilter
http/@jaas-api-provision
REMEMBER_ME_FILTER
RememberMeAuthenticationFilter
http/remember-me
ANONYMOUS_FILTER
AnonymousAuthenticationFilter
http/anonymous
SESSION_MANAGEMENT_FILTER
SessionManagementFilter
session-management
EXCEPTION_TRANSLATION_FILTER
ExceptionTranslationFilter
http
FILTER_SECURITY_INTERCEPTOR
FilterSecurityInterceptor
http
SWITCH_USER_FILTER
SwitchUserFilter
N/A
UsernamePasswordAuthenticationFilter UsernamePasswordAuthenticationFilter继承虚拟类AbstractAuthenticationProcessingFilter。
AbstractAuthenticationProcessingFilter要求设置一个authenticationManager,authenticationManager的实现类将实际处理请求的认证。
AbstractAuthenticationProcessingFilter将拦截符合过滤规则的request,并试图执行认证。子类必须实现 attemptAuthentication 方法,这个方法执行具体的认证。
认证处理:如果认证成功,将会把返回的Authentication对象存放在SecurityContext;然后setAuthenticationSuccessHandler(AuthenticationSuccessHandler)方法将会调用;这里处理认证成功后跳转url的逻辑;可以重新实现AuthenticationSuccessHandler的onAuthenticationSuccess方法,实现自己的逻辑,比如需要返回json格式数据时,就可以在这里重新相关逻辑。如果认证失败,默认会返回401代码给客户端,当然也可以在节点中配置失败后跳转的url,还可以重写AuthenticationFailureHandler的onAuthenticationFailure方法实现自己的逻辑。
UsernamePasswordAuthenticationFilter 用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,对应的参数名默认为 j_username 和 j_password。如果不想使用默认的参数名,可以通过 UsernamePasswordAuthenticationFilter 的 usernameParameter 和 passwordParameter 进行指定。表单的提交路径默认是 “j_spring_security_check”,也可以通过 UsernamePasswordAuthenticationFilter 的 filterProcessesUrl 进行指定。通过属性 postOnly 可以指定只允许登录表单进行 post 请求,默认是 true。其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler 和 AuthenticationFailureHandler,这些都可以根据需求做相关改变。此外,它还需要一个 AuthenticationManager 的引用进行认证,这个是没有默认配置的。
自定义 Filter 自定义的 Filter 建议继承 GenericFilterBean,示例:
1 2 3 4 5 6 7 8 9 public class BeforeLoginFilter extends GenericFilterBean { @Override public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("This is a filter before UsernamePasswordAuthenticationFilter." ); filterChain.doFilter(servletRequest, servletResponse); } }
配置自定义 Filter 在 Spring Security 过滤器链中的位置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 protected void configure (HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/" ).permitAll() .antMatchers("/user/**" ).hasRole("USER" ) .and() .formLogin().loginPage("/login" ).defaultSuccessUrl("/user" ) .and() .logout().logoutUrl("/logout" ).logoutSuccessUrl("/login" ); http.addFilterBefore(new BeforeLoginFilter(), UsernamePasswordAuthenticationFilter.class); http.addFilterAfter(new AfterCsrfFilter(), CsrfFilter.class); }
HttpSecurity 有三个常用方法来配置:
addFilterBefore(Filter filter, Class<? extends Filter> beforeFilter):在 beforeFilter 之前添加 filter
addFilterAfter(Filter filter, Class<? extends Filter> afterFilter):在 afterFilter 之后添加 filter
addFilterAt(Filter filter, Class<? extends Filter> atFilter):在 atFilter 相同位置添加 filter, 此 filter 不覆盖 filter
通过在不同 Filter 的 doFilter() 方法中加断点调试,可以判断哪个 filter 先执行,从而判断 filter 的执行顺序 。
logout 1 .logout().clearAuthentication(true ).invalidateHttpSession(true ).logoutUrl("/logout" ).logoutSuccessUrl("/home" ).permitAll();
实例 依赖 spring-boot-starter-security
实例一 配置文件中配置:
1 2 3 4 5 spring: security: user: name: hearing password: 123456
无论访问哪一个页面,都需要认证,跳转到login页面,使用配置的用户名和密码登录.
实例二 添加SpringSecurity配置类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure (AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .passwordEncoder(new BCryptPasswordEncoder()) .withUser("hearing" ) .password(new BCryptPasswordEncoder().encode("123456" )) .roles("role1" ) .and() .withUser("hhh" ) .password(new BCryptPasswordEncoder().encode("123456" )) .roles("role2" ); } }
无论访问哪一个页面,都需要认证,跳转到login页面,使用配置的两个用户名和密码登录.
实例三
使用数据库中的记录进行登录
自定义登录界面
自定义认证页面
自定义登录成功/失败处理
添加SpringSecurity配置类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private LoginSuccessHandler loginSuccessHandler; @Override protected void configure (HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests() .antMatchers("/" , "/home" ).permitAll() .antMatchers("/auth" ).authenticated() .antMatchers("/admin" ).hasAuthority("role1" ) .anyRequest().authenticated() .and() .formLogin() .loginPage("/login" ).loginProcessingUrl("/loginConfirm" ) .usernameParameter("name" ).passwordParameter("password" ) .defaultSuccessUrl("/home" ).successHandler(loginSuccessHandler) .permitAll() .and() .logout().clearAuthentication(true ).invalidateHttpSession(true ).logoutUrl("/logout" ).logoutSuccessUrl("/home" ) .permitAll(); } @Override protected void configure (AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(new UserService()).passwordEncoder(new BCryptPasswordEncoder()); } }
1 2 3 4 5 6 7 8 9 10 11 12 @Component public class LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { @Override public void onAuthenticationSuccess (HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { System.out.println("onAuthenticationSuccess..." ); System.out.println(((UserDetails)(authentication.getPrincipal())).getUsername()); getRedirectStrategy().sendRedirect(request, response, "loginSuccess" ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 public class User implements UserDetails { private String name; private String password; private Boolean enable; private List<String> roles; public User (String name, String password, Boolean enable, List<String> roles) { this .name = name; this .password = password; this .enable = enable; this .roles = roles; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { if (roles == null || roles.size() < 1 ) { return AuthorityUtils.commaSeparatedStringToAuthorityList("" ); } List<GrantedAuthority> grantedAuthorities = new ArrayList<>(); for (String role : roles) { grantedAuthorities.add(new SimpleGrantedAuthority(role)); } return grantedAuthorities; } @Override public String getPassword () { return password; } @Override public String getUsername () { return name; } @Override public boolean isAccountNonExpired () { return enable; } @Override public boolean isAccountNonLocked () { return enable; } @Override public boolean isCredentialsNonExpired () { return enable; } @Override public boolean isEnabled () { return enable; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class UserService implements UserDetailsService { @Override public UserDetails loadUserByUsername (String s) throws UsernameNotFoundException { System.out.println("loadUserByUsername..." ); List<String> roles = new ArrayList<>(); if (s.equals("hearing" )) { roles.add("role1" ); return new User(s, new BCryptPasswordEncoder().encode("123456" ), true , roles); } else if (s.equals("hhh" ) ){ return new User(s, new BCryptPasswordEncoder().encode("123456" ), true , roles); } else { return null ; } } }
用户只有通过上述UserService中的两个用户信息来登录,此处可以换成由数据中来获取对应的UserDetails对象(因为是通过用户名来查找数据库,所以数据表中用户名应该唯一).
实例四
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private LoginSuccessHandler loginSuccessHandler; @Autowired private MyFilterSecurityInterceptor myFilterSecurityInterceptor; @Override protected void configure (HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests() .antMatchers("/" , "/home" ).permitAll() .antMatchers("/auth" ).authenticated() .antMatchers("/test" ).authenticated() .antMatchers("/admin" ).authenticated() .and() .formLogin() .loginPage("/login" ).loginProcessingUrl("/loginConfirm" ) .usernameParameter("name" ).passwordParameter("password" ) .defaultSuccessUrl("/home" ).successHandler(loginSuccessHandler) .permitAll() .and() .logout().clearAuthentication(true ).invalidateHttpSession(true ).logoutUrl("/logout" ).logoutSuccessUrl("/home" ) .permitAll(); http.exceptionHandling().accessDeniedPage("/deny" ); http.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class); } @Override protected void configure (AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(new UserService()).passwordEncoder(new BCryptPasswordEncoder()); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Service public class MyAccessDecisionManager implements AccessDecisionManager { @Override public void decide (Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { if (null == configAttributes || configAttributes.size() <=0 ) { return ; } ConfigAttribute c; String needRole; for (Iterator<ConfigAttribute> iter = configAttributes.iterator(); iter.hasNext(); ) { c = iter.next(); needRole = c.getAttribute(); for (GrantedAuthority ga : authentication.getAuthorities()) { if (needRole.trim().equals(ga.getAuthority())) { return ; } } } throw new AccessDeniedException("no right" ); } @Override public boolean supports (ConfigAttribute attribute) { return true ; } @Override public boolean supports (Class<?> clazz) { return true ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 @Service public class MyInvocationSecurityMetadataSourceService implements FilterInvocationSecurityMetadataSource { private HashMap<String, Collection<ConfigAttribute>> map = null ; public void loadResourceDefine () { map = new HashMap<>(); Collection<ConfigAttribute> array = new ArrayList<>(); ConfigAttribute cfg = new SecurityConfig("role1" ); array.add(cfg); map.put("/admin" , array); } @Override public Collection<ConfigAttribute> getAttributes (Object object) throws IllegalArgumentException { if (map == null ) { loadResourceDefine(); } HttpServletRequest request = ((FilterInvocation) object).getHttpRequest(); AntPathRequestMatcher matcher; String resUrl; for (Iterator<String> iter = map.keySet().iterator(); iter.hasNext(); ) { resUrl = iter.next(); matcher = new AntPathRequestMatcher(resUrl); if (matcher.matches(request)) { return map.get(resUrl); } } return null ; } @Override public Collection<ConfigAttribute> getAllConfigAttributes () { return null ; } @Override public boolean supports (Class<?> clazz) { return true ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 @Service public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter { @Autowired private FilterInvocationSecurityMetadataSource securityMetadataSource; @Autowired public void setMyAccessDecisionManager (MyAccessDecisionManager myAccessDecisionManager) { super .setAccessDecisionManager(myAccessDecisionManager); } @Override public void init (FilterConfig filterConfig) throws ServletException { } @Override public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FilterInvocation fi = new FilterInvocation(request, response, chain); invoke(fi); } private void invoke (FilterInvocation fi) throws IOException, ServletException { InterceptorStatusToken token = super .beforeInvocation(fi); try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super .afterInvocation(token, null ); } } @Override public void destroy () { } @Override public Class<?> getSecureObjectClass() { return FilterInvocation.class; } @Override public SecurityMetadataSource obtainSecurityMetadataSource () { return this .securityMetadataSource; } }
实例五
建立数据表:
1 2 3 4 5 6 CREATE TABLE persistent_logins ( username VARCHAR (64 ) NOT NULL , series VARCHAR (64 ) NOT NULL PRIMARY KEY , token VARCHAR (64 ) NOT NULL , last_used TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private LoginSuccessHandler loginSuccessHandler; @Autowired @Qualifier("dataSource") private DataSource dataSource; @Autowired private MyFilterSecurityInterceptor myFilterSecurityInterceptor; @Bean public RememberMeServices rememberMeServices () { JdbcTokenRepositoryImpl rememberMeTokenRepository = new JdbcTokenRepositoryImpl(); rememberMeTokenRepository.setDataSource(dataSource); PersistentTokenBasedRememberMeServices rememberMeServices = new PersistentTokenBasedRememberMeServices("remember" , new UserService(), rememberMeTokenRepository); rememberMeServices.setParameter("remember-me" ); return rememberMeServices; } @Override protected void configure (HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests() .antMatchers("/" , "/home" ).permitAll() .antMatchers("/auth" ).authenticated() .antMatchers("/test" ).authenticated() .antMatchers("/admin" ).authenticated() .and() .rememberMe().rememberMeServices(rememberMeServices()).key("remember" ) .and() .formLogin() .loginPage("/login" ).loginProcessingUrl("/loginConfirm" ) .usernameParameter("name" ).passwordParameter("password" ) .defaultSuccessUrl("/home" ).successHandler(loginSuccessHandler) .permitAll() .and() .logout().clearAuthentication(true ).invalidateHttpSession(true ).logoutUrl("/logout" ).logoutSuccessUrl("/home" ) .permitAll(); http.exceptionHandling().accessDeniedPage("/deny" ); http.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class); } @Override protected void configure (AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(new UserService()).passwordEncoder(new BCryptPasswordEncoder()); } }