关于springsecurity:Spring-Security6-全新写法大变样

@[toc]Spring Security 在最近几个版本中配置的写法都有一些变动,很多常见的办法都废除了,并且将在将来的 Spring Security7 中移除,因而松哥在去年旧文的根底之上,又补充了一些新的内容,从新发一下,供各位应用 Spring Security 的小伙伴们参考。 接下来,我把从 Spring Security5.7 开始(对应 Spring Boot2.7 开始),各种已知的变动都来和小伙伴们梳理一下。 1. WebSecurityConfigurerAdapter 首先第一点,就是各位小伙伴最容易发现的 WebSecurityConfigurerAdapter 过期了,在目前最新的 Spring Security6.1 中,这个类曾经齐全被移除了,想凑合着用都不行了。 精确来说,Spring Security 是在 5.7.0-M2 这个版本中将 WebSecurityConfigurerAdapter 过期的,过期的起因是因为官网想要激励各位开发者应用基于组件的平安配置。 那么什么是基于组件的平安配置呢?咱们来举几个例子: 以前咱们配置 SecurityFilterChain 的形式是上面这样: @Configurationpublic class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeHttpRequests((authz) -> authz .anyRequest().authenticated() ) .httpBasic(withDefaults()); }}那么当前就要改为上面这样了: @Configurationpublic class SecurityConfiguration { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests((authz) -> authz .anyRequest().authenticated() ) .httpBasic(withDefaults()); return http.build(); }}如果懂之前的写法的话,上面这个代码其实是很好了解的,我就不做过多解释了,不过还不懂 Spring Security 根本用法的小伙伴,能够在公众号后盾回复 ss,有松哥写的教程。 ...

June 14, 2023 · 7 min · jiezi

关于springsecurity:创建SpringSecurity项目

前言 在上一章节中,曾经带大家意识了Spring Security,对其基本概念已有所理解,然而作为一个合格的程序员,最要害的必定还是得动起手来,所以从本篇文章开始,我就带大家搭建第一个Spring Security我的项目,看看如何利用Spring Security来爱护咱们的Java Web我的项目。 一. 搭建SpringBoot开发环境 咱们的Spring Security系列教程会基于SpringBoot环境,并且以案例迭代的形式进行开发,所以为了不便后续案例的编写,咱们先提前搭建一个SpringBoot环境的Web我的项目。 1.创立SpringBoot我的项目如各位对SpringBoot根底不相熟,请参考自己的SpringBoot系列教程:blog.csdn.net/syc000666/a…SpringBoot我的项目的具体创立过程如下图所示。 1.1 创立一个基于Maven的Project我的项目。 1.2 设置项目名称和存储地位 2.增加我的项目依赖在pom.xml文件中,增加配置SpringBoot开发环境的依赖包。 <properties> <java.version>1.8</java.version><maven.compiler.source>1.8</maven.compiler.source><maven.compiler.target>1.8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><spring-boot.version>2.2.5.RELEASE</spring-boot.version><spring-platform.version>Cairo-SR3</spring-platform.version></properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency><!--BOM(bill of materials):资料清单,用于解决jar包依赖的好办法--> <!--缘起:Spring当初已倒退成一个宏大体系。比方security、mvc等。如此一来,不同模块或者与内部进行集成时, 依赖解决就须要各自对应版本号。比方,较新spring与较老的quartz,它们集成就会遇到问题,给搭建和降级带来不便。 因而Spring IO Platform应运而生,只有我的项目中引入了它,内部集成时依赖关系无需版本号。--> <dependency> <groupId>io.spring.platform</groupId> <artifactId>platform-bom</artifactId> <version>${spring-platform.version}</version> <type>pom</type> <scope>import</scope> </dependency></dependencies></dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency></dependencies><build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins></build><!--配置地方仓库--><repositories> <repository> <id>aliyun-repos</id> <url>https://maven.aliyun.com/repository/public</url> <snapshots> <enabled>false</enabled> </snapshots> </repository></repositories> 复制代码增加完SpringBoot中次要的依赖包之后,咱们就能够在这个环境中进行Web我的项目开发了。 二. 创立第一个SpringSecurity我的项目咱们在下面的SpringBoot开发环境中,创立出第一个SpringSecurity模块,具体创立过程略(嘿嘿)。 1.增加模块中的pom依赖咱们在该module中,增加我的项目开发时必要的依赖包,次要是增加SpringSecurity的依赖包,在这里会完满体现SpringBoot中”约定大于配置“的思维哦。 <dependencies> <dependency> ...

April 12, 2023 · 1 min · jiezi

关于springsecurity:Spring-Security-中的核心对象

Spring Security 的外围对象实用于 Spring Security 5.4.x 以上版本.SecurityFilterChain依据匹配规定Spring Security 中的过滤器链对象, 在没有自定义 SecurityFilterChain 注入Ioc 容器时,在Spring Boot 主动配置类中,默认向 Ioc 容器中注入一个 defaultSecurityFilterchain 对象. 通过@ConditionalOnDefaultWebSecurity 注解实现该成果. 默认 SecurityFilterChain 按程序执行上面的过滤器: FilterChainProxySecurity 的所有过滤器的 代理类, 实现Filter 接口.HttpSecurity用于构建过滤器链的对象HttpSecurity 被 @Scope("protopye") 润饰,因而在其余 Bean 中注入 HttpSecurity Bean 时,会以该办法创立的对象作为原型,创立一个新的 HttpSecurity. 也称为多例Bean. 在构建每个过滤器链 SecurityFilterChain 时,都会创立一个新的 HttpSecurity. WebSecurity用于构建 FilterChainProxy Bean. Spring Boot 对 Security 的自动化配置UserDetailsServiceAutoConfiguration配置默认的用户管理器: InMemoryUserDetailsManager 详情参考:[源码分析用户信息的管理者 --userdetailsmanager

March 2, 2023 · 1 min · jiezi

关于springsecurity:源码剖析用户信息的管理者UserDetailsManager

Spring Security 的作为守门员,其两大性能:认证(Authentication) 和 受权(authorization)学而思: Spring Security 是如何对用户进行治理的?初始化我的项目并启动初始化一个 Spring Boot 我的项目并编写一个接口,在没有引入 Spring Security 依赖时,接口是可能能失常拜访的。 @RestController@RequestMapping("/user")public class UmsAdminController { @GetMapping("/details") public Object userInfos() { return "用户详情"; }}<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency><dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope></dependency>在引入依赖后,再次申请接口 http://localhost:8080/user/details ,页面跳转至了登录页面。 用户默认名为 user , 默认明码是每次启动利用时随机生成的,启动利用时,显示在了控制台中。 UserDetailsManager明明什么都没干,只是引入平安的框架,为什么申请接口就被拦挡?这就与 Spring Security 的机制无关,Spring Security 采纳了”默认回绝“ 的安全策略。意思是资源默认对未认证受权用户禁止拜访。为须要凋谢的资源进行属性配置,而不是默认对资源进行凋谢,这被认为是一种良好的平安做法。 在申请接口前,Spring Security 采纳了过滤器对接口进行拦挡认证(过滤器是Security 的外围局部,这个之后再详述)。认证过程中用到 UserDetailsManager ,也就是本章的配角。 UserDetailsManager 是治理用户的接口,继承了 UserDetailsService 接口,UserDetailsService 提供了加载用户详细信息(UserDetails)的办法(loadUserByUsername) 。 UserDetails: 用户详细信息的接口,对用户的形象,提供了获取用户名和明码等办法UserDetailsService:提供了一个加载用户的办法,该办法依据用户名从存储(内存、数据库等)中查问出用户对象(UserDetails),认证过程最终就是通过调用该办法,来认证用户。UserDetailsManager:在 UserDetailsService 根底上减少了 增删改等性能,实现了对用户的治理。 两个实现类在Spring Security 中 ,UserDetailsManager 有两个实现类,如下图: InMemoryUserDetailsManager:基于内存的用户管理器,我的项目初始化时默认应用,不能满足业务需要JdbcUserDetailsManager:基于数据库的用户管理器,更合乎实在业务的需要 InMemoryUserDetailsManager先来摸索 InMemoryUserDetailsManager 在Security 中是如何构建的。 ...

March 2, 2023 · 1 min · jiezi

关于springsecurity:Spring-Security实现多种认证方式

一、引言理论零碎通常须要实现多种认证形式,比方用户名明码、手机验证码、邮箱等等。Spring Security能够通过自定义认证器AuthenticationProvider 来实现不同的认证形式。接下来介绍一下SpringSecurity具体如何来实现多种认证形式。 二、具体步骤这里咱们以用户名明码、手机验证码两种形式来进行演示,其余一些登录形式相似。 2.1 自定义认证器AuthenticationProvider首先针对每一种登录形式,咱们能够定义其对应的认证器AuthenticationProvider,以及对应的认证信息Authentication,理论场景中这两个个别是配套应用。认证器AuthenticationProvider有一个认证办法authenticate(),咱们须要实现该认证办法,认证胜利之后返回认证信息Authentication。 2.1.1 手机验证码针对手机验证码形式,咱们能够定义以下两个类MobilecodeAuthenticationProvider.class import com.kamier.security.web.service.MyUser;import org.springframework.security.authentication.AuthenticationProvider;import org.springframework.security.authentication.BadCredentialsException;import org.springframework.security.core.Authentication;import org.springframework.security.core.AuthenticationException;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.security.core.userdetails.UsernameNotFoundException;import java.util.HashMap;import java.util.Map;public class MobilecodeAuthenticationProvider implements AuthenticationProvider { private UserDetailsService userDetailsService; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { MobilecodeAuthenticationToken mobilecodeAuthenticationToken = (MobilecodeAuthenticationToken) authentication; String phone = mobilecodeAuthenticationToken.getPhone(); String mobileCode = mobilecodeAuthenticationToken.getMobileCode(); System.out.println("登陆手机号:" + phone); System.out.println("手机验证码:" + mobileCode); // 模仿从redis中读取手机号对应的验证码及其用户名 Map<String, String> dataFromRedis = new HashMap<>(); dataFromRedis.put("code", "6789"); dataFromRedis.put("username", "admin"); // 判断验证码是否统一 if (!mobileCode.equals(dataFromRedis.get("code"))) { throw new BadCredentialsException("验证码谬误"); } // 如果验证码统一,从数据库中读取该手机号对应的用户信息 MyUser loadedUser = (MyUser) userDetailsService.loadUserByUsername(dataFromRedis.get("username")); if (loadedUser == null) { throw new UsernameNotFoundException("用户不存在"); } else { MobilecodeAuthenticationToken result = new MobilecodeAuthenticationToken(loadedUser, null, loadedUser.getAuthorities()); return result; } } @Override public boolean supports(Class<?> aClass) { return MobilecodeAuthenticationToken.class.isAssignableFrom(aClass); } public void setUserDetailsService(UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; }}留神这里的supports办法,是实现多种认证形式的要害,认证管理器AuthenticationManager会通过这个supports办法来断定以后须要应用哪一种认证形式。 ...

February 24, 2023 · 4 min · jiezi

关于springsecurity:Spring-Security-Vue-Flowable-怎么玩

@[toc]之前松哥发过一篇文章,和小伙伴们分享了 Spring Boot+Vue+Flowable 的具体玩法,传送门: SpringBoot+Vue+Flowable,模仿一个销假审批流程!不过在那篇文章中,所有波及到用户的中央,都是手动输出的,这显然不太好,例如开启一个销假流程: 这个流程中须要用户输出本人的姓名,其实如果以后用户登录了,就不必输出用户名了,间接就应用以后登录的用户名。 另一方面,当咱们引入了用户零碎之后,当用户提交销假申请的时候,也能够指定审批人,这样看起来就更实在了。 所以,明天咱们就整篇文章,咱们引入 Spring Security,据此来构建用户零碎,一起来看下有了用户零碎的流程引擎该是什么样子。 1. 成果展现货色我曾经做好了,先截个图给大家看下: 这个页面分了三局部: 最下面的是销假申请,用户只须要填入销假天数、销假理由,并且抉择审批人即可,抉择审批人的时候,能够间接指定审批人的名字,也能够抉择审批人的角色,例如抉择经理这个角色,那么未来只有角色为经理的任意用户登录胜利之后,就能够看到本人须要审批的销假了。两头的列表展现以后登录用户已经提交过的销假申请,这些申请的状态分为三种,别离是已通过、已回绝以及待审批。上面的列表是这个用户须要审批的其余用户提交的销假申请,图片中这个用户暂无要审批的工作,如果有的话,这个中央会通过表格展现进去,表格中每一行有批准和回绝两个按钮,点击之后就能够实现本人的操作了。这就是咱们这次要实现的成果了,相比于SpringBoot+Vue+Flowable,模仿一个销假审批流程!文章的案例,这次的显然看起来更像一回事,不过本文的案例是在上篇文章案例的根底上实现的,没看过上篇文章的小伙伴倡议先看下上篇文章,上篇文章中的案例,大家能够在微信公众号江南一点雨的后盾回复 flowable02 获取。 2. 两套用户体系玩过工作流的小伙伴应该都晓得,工作流中其实自带了一套用户零碎,然而咱们本人的零碎往往也有本人的用户体系,那么如何将两者交融起来呢?或者说是否有必要将两者交融起来呢? 如果你想将本人零碎的用户体系和 flowable 中的用户体系交融起来,那么整体上来说,大略就是两种方法吧: 咱们能够以本人零碎中的用户体系为准(因为 flowable 本人的用户体系字段往往不能满足咱们的需要),而后创立对应的视图即可。例如 flowable 中的用户表 ACT_ID_USER、分组表 ACT_ID_GROUP、用户分组关联表 ACT_ID_MEMBERSHIP 等等,把这些和用户体系相干的表删除掉,而后依据这些表的字段和名称,联合本人的零碎用户,创立与之雷同的视图。利用 IdentityService 这个服务,当咱们要操作本人的零碎用户的时候,例如增加、更新、删除用户的时候,顺便调用 IdentityService 服务增加、更新、删除 flowable 中的用户。这两种思路其实都不难,也都很好实现,然而有没有可能咱们就间接舍弃掉 flowable 中的用户体系间接用本人的用户体系呢?在松哥目前的我的项目中,这条路目前是行得通的,就是将 flowable 的用户体系抛到一边,当做没有,只用本人零碎的用户体系。 如果在读这篇文章的小伙伴中,有人在本人的零碎中,有场景必须用到 flowable 自带的用户体系,欢送留言探讨。本文松哥和小伙伴们展现的案例,就是完完全全应用了本人的用户体系,没有用 flowable 中的那一套用户体系。 好啦,这个问题捋分明了,接下来咱们就开搞! 3. 创立用户表首先咱们来创立三张表,别离是用户表 user、角色表 role 以及用户角色关联表 user_role,脚本如下: SET NAMES utf8mb4;DROP TABLE IF EXISTS `role`;CREATE TABLE `role` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `nameZh` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;INSERT INTO `role` (`id`, `name`, `nameZh`)VALUES (1,'manager','经理'), (2,'team_leader','组长');DROP TABLE IF EXISTS `user`;CREATE TABLE `user` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `username` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;INSERT INTO `user` (`id`, `username`, `password`)VALUES (1,'javaboy','{noop}123'), (2,'zhangsan','{noop}123'), (3,'lisi','{noop}123'), (4,'江南一点雨','{noop}123');DROP TABLE IF EXISTS `user_role`;CREATE TABLE `user_role` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `uid` int(11) DEFAULT NULL, `rid` int(11) DEFAULT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;INSERT INTO `user_role` (`id`, `uid`, `rid`)VALUES (1,1,1), (2,4,1), (3,2,2), (4,3,2);我也大略说下我的用户: ...

August 22, 2022 · 4 min · jiezi

关于springsecurity:SpringSecurityOauth2搭建资源服务

前言之前写过应用springsecurity搭建认证服务SpringSecurity+Oauth2:明码受权模式获取Token(MongoDB+JWT),这篇则写对于如何搭建资源服务,只有验证通过的token能力拜访。 操作1、配置Pom.xml援用spring-cloud-security和oauth2的jar包2、配置主类@EnableResourceServer注解,开启资源服务3、创立JWTTokenStoreConfig类,配置和解析token4、创立ResourceServerConfiguration类配置拜访权限以及自定义异样5、自定义springsecurity异样信息(留神:认证和资源服务的自定义异样是对立的没有区别,上面会阐明) 1、配置Pom.xml援用spring-cloud-security和oauth2的jar包<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> <version>2.2.5.RELEASE</version></dependency><dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-security</artifactId> <version>2.2.5.RELEASE</version></dependency>2、配置主类@EnableResourceServer注解,开启资源服务@SpringBootApplication// 资源爱护服务@EnableResourceServer// 服务发现@EnableDiscoveryClient@EnableFeignClients@RefreshScopepublic class Xxxx { @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } public static void main(String[] args) { SpringApplication.run(Xxxx.class, args); }}3、创立JWTTokenStoreConfig类,配置和解析token@Configurationpublic class JWTTokenStoreConfig { @Autowired private ServiceConfig serviceConfig; //JWT @Bean public TokenStore tokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); } //JWT @Bean @Primary public DefaultTokenServices tokenServices() { DefaultTokenServices defaultTokenServices = new DefaultTokenServices(); defaultTokenServices.setTokenStore(tokenStore()); defaultTokenServices.setSupportRefreshToken(true); return defaultTokenServices; } //JWT // 从配置文件中获取jwt key,而后本人解析token是否无效, @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey(serviceConfig.getJwtSigningKey()); return converter; }}4、创立ResourceServerConfiguration类配置拜访权限以及自定义异样@Configurationpublic class ResourceServerConfiguration extends ResourceServerConfigurerAdapter{ @Override public void configure(ResourceServerSecurityConfigurer resources) { OAuth2AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint(); authenticationEntryPoint.setExceptionTranslator(new CustomOAuthWebResponseExceptionTranslator()); resources.authenticationEntryPoint(authenticationEntryPoint); OAuth2AccessDeniedHandler oAuth2AccessDeniedHandler = new OAuth2AccessDeniedHandler(); oAuth2AccessDeniedHandler.setExceptionTranslator(new CustomOAuthWebResponseExceptionTranslator()); resources.accessDeniedHandler(oAuth2AccessDeniedHandler); } @Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 放开/sendRegisterEmailMessage,不须要校验token .antMatchers("/websocket/**").permitAll() .antMatchers("/info/**").permitAll() .anyRequest().authenticated(); }}5、自定义springsecurity异样信息创立CustomOAuthException类 ...

July 18, 2022 · 2 min · jiezi

关于springsecurity:SpringSecurity能否吊打Shiro

文章内容:SpringSecurity和Shiro区别及用法 作者:优极限 Apache Shiro是一个弱小且易用的Java平安框架,可能十分清晰的解决认证、受权、治理会话以及明码加密。应用Shiro的易于了解的API,您能够疾速、轻松地取得任何应用程序,从最小的挪动应用程序到最大的网络和企业应用程序。 执行流程 特点易于了解的 Java Security API;简略的身份认证(登录),反对多种数据源(LDAP,JDBC,Kerberos,ActiveDirectory 等);对角色的简略的签权(访问控制),反对细粒度的签权;反对一级缓存,以晋升应用程序的性能;内置的基于 POJO 企业会话治理,实用于 Web 以及非 Web 的环境;异构客户端会话拜访;非常简单的加密 API;不跟任何的框架或者容器捆绑,能够独立运行。Spring Security Spring Security 次要实现了Authentication(认证,解决who are you? ) 和 Access Control(访问控制,也就是what are you allowed to do?,也称为Authorization)。Spring Security在架构上将认证与受权拆散,并提供了扩大点。它是一个轻量级的平安框架,它确保基于Spring的应用程序提供身份验证和受权反对。它与Spring MVC有很好地集成 ,并装备了风行的平安算法实现捆绑在一起。 执行流程 客户端发动一个申请,进入 Security 过滤器链。当到 LogoutFilter 的时候判断是否是登出门路,如果是登出门路则到 logoutHandler ,如果登出胜利则到 logoutSuccessHandler 登出胜利解决,如果登出失败则由 ExceptionTranslationFilter ;如果不是登出门路则间接进入下一个过滤器。当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录门路,如果是,则进入该过滤器进行登录操作,如果登录失败则到 AuthenticationFailureHandler 登录失败处理器解决,如果登录胜利则到 AuthenticationSuccessHandler 登录胜利处理器解决,如果不是登录申请则不进入该过滤器。当到 FilterSecurityInterceptor 的时候会拿到 uri ,依据 uri 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权胜利则到 Controller 层否则到 AccessDeniedHandler 鉴权失败处理器解决。特点shiro能实现的,Spring Security 根本都能实现,依赖于Spring体系,然而益处是Spring全家桶的亲儿子,集成上更加符合,在应用上,比shiro略负责。 两者比照 Shiro比Spring Security更容易应用,也就是实现上简略一些,同时根本的受权认证Shiro也根本够用 Spring Security社区反对度更高,Spring社区的亲儿子,反对力度和更新保护上有劣势,同时和Spring这一套的联合较好。 ...

January 21, 2022 · 1 min · jiezi

关于springsecurity:spring-securityoauth2-实现统一认证鉴权平台客户端sdk部分

接入简略,反对多租户,可配置,低代码式的疾速实现一套本人的认证鉴权逻辑 蕴含用户治理、部门治理、角色治理、权限治理,将来还能够反对更多模块,比方数据字典治理、... 预计节俭每个利用相干模块研发工夫5人天 对于oauth2.0协定来说,有几个概念 受权服务器客户端资源服务器这偏文章次要讲客户端sdk实现,资源服务器次要就是对立寄存用户信息的,受权服务器应用springsecurity官网的进行小定制。 首先应用springboot的主动配置性能,次要配置类有SecurityConfig,通过编写META-INF/spring.factories来实现主动加载配置bean @Configuration(proxyBeanMethods = false)@EnableWebSecurity@EnableConfigurationProperties(OauthProperties.class)public class SecurityConfig { @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SecurityFilterChain oauth2SecurityFilterChain(HttpSecurity http, IntrospectorFilter filter, CustomLoginUrlAuthenticationEntryPoint entryPoint, ClientRegistrationRepository clientRegistrationRepository, CustomRequestCache customRequestCache) throws Exception { ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry expressionInterceptUrlRegistry = http.authorizeRequests(); if(!CollectionUtils.isEmpty(oauthProperties.getIgnoreUris())) { expressionInterceptUrlRegistry.requestMatchers(oauthProperties.getIgnoreUris().stream().map(AntPathRequestMatcher::new).toArray(RequestMatcher[]::new)).permitAll(); } expressionInterceptUrlRegistry.anyRequest().authenticated().and() .oauth2Login().authorizationEndpoint().authorizationRequestResolver(authorizationRequestResolver(clientRegistrationRepository)) .and() // .defaultSuccessUrl("/", true); .successHandler(authenticationSuccessHandler()); http.oauth2Client().and() .cors().and() .exceptionHandling().authenticationEntryPoint(entryPoint).and() .csrf().disable() .requestCache(requestCacheCustomizer->{ requestCacheCustomizer.requestCache(customRequestCache.getRequestCache(http)); }); http.addFilterBefore(filter, AnonymousAuthenticationFilter.class); return http.build(); } @Bean public CustomRequestCache customRequestCache() { return new CustomRequestCache(); }; private AuthenticationSuccessHandler authenticationSuccessHandler() { SavedRequestAwareAuthenticationSuccessHandler successHandler = new CustomSavedRequestAwareAuthenticationSuccessHandler(); successHandler.setUseReferer(true); return successHandler; } private OAuth2AuthorizationRequestResolver authorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository) { DefaultOAuth2AuthorizationRequestResolver authorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver( clientRegistrationRepository, "/oauth2/authorization"); authorizationRequestResolver.setAuthorizationRequestCustomizer( authorizationRequestCustomizer()); return authorizationRequestResolver; } private Consumer<OAuth2AuthorizationRequest.Builder> authorizationRequestCustomizer() { return customizer -> customizer .additionalParameters(params -> params.put("tenantId", oauthProperties.getClientId())); } @Bean public CustomLoginUrlAuthenticationEntryPoint customLoginUrlAuthenticationEntryPoint(ObjectMapper objectMapper) { CustomLoginUrlAuthenticationEntryPoint customLoginUrlAuthenticationEntryPoint = new CustomLoginUrlAuthenticationEntryPoint("/oauth2/authorization/auth_server"); customLoginUrlAuthenticationEntryPoint.setObjectMapper(objectMapper); customLoginUrlAuthenticationEntryPoint.setOauthProperties(oauthProperties); return customLoginUrlAuthenticationEntryPoint; }; @Bean public IntrospectorFilter introspectorFilter(OpaqueTokenIntrospector opaqueTokenIntrospector, OAuth2AuthorizedClientService oAuth2AuthorizedClientService) { IntrospectorFilter introspectorFilter = new IntrospectorFilter(); introspectorFilter.setOpaqueTokenIntrospector(opaqueTokenIntrospector); introspectorFilter.setoAuth2AuthorizedClientService(oAuth2AuthorizedClientService); return introspectorFilter; } @Bean public NimbusOpaqueTokenIntrospector opaqueTokenIntrospector() { String introspectUri = oauthProperties.getIssuerUri() + "/oauth2/introspect"; OAuth2ResourceServerProperties.Opaquetoken opaqueToken = new OAuth2ResourceServerProperties.Opaquetoken(); opaqueToken.setIntrospectionUri(introspectUri); opaqueToken.setClientId(oauthProperties.getClientId()); opaqueToken.setClientSecret(oauthProperties.getClientSecret()); return new NimbusOpaqueTokenIntrospector(opaqueToken.getIntrospectionUri(), opaqueToken.getClientId(), opaqueToken.getClientSecret()); } //@RefreshScope 动静刷新 @Bean @ConditionalOnMissingBean public ClientRegistrationRepository clientRegistrationRepository() { return new InMemoryClientRegistrationRepository(this.clientRegistration()); } @Bean public OidcUserService oidcUserService(DefaultOAuth2UserService defaultOAuth2UserService) { CustomOidcUserService oidcUserService = new CustomOidcUserService(); oidcUserService.setAccessibleScopes0(scopes); oidcUserService.setOauth2UserService0(defaultOAuth2UserService); return oidcUserService; } @ConditionalOnMissingBean @Bean public ParameterizedTypeReference<Result<OpenAppUser<Map<String, Object>>>> userInfoTypeReference () { return new ParameterizedTypeReference<Result<OpenAppUser<Map<String, Object>>>>() {}; } @Bean public DefaultOAuth2UserService defaultOAuth2UserService(ParameterizedTypeReference userInfoTypeReference) { CustomDefaultOAuth2UserService defaultOAuth2UserService = new CustomDefaultOAuth2UserService(); defaultOAuth2UserService.setPARAMETERIZED_RESPONSE_TYPE(userInfoTypeReference); return defaultOAuth2UserService; } @Autowired OauthProperties oauthProperties; private Set<String> scopes = new HashSet(){ { //add("openid"); add("user"); } }; private ClientRegistration clientRegistration() { if (oauthProperties.getScopes()!=null) scopes.addAll(oauthProperties.getScopes()); ClientRegistration.Builder auth_server = ClientRegistrations.fromIssuerLocation(oauthProperties.getIssuerUri()).registrationId("auth_server"); return auth_server.clientId(oauthProperties.getClientId()) .clientSecret(oauthProperties.getClientSecret()) .clientName(oauthProperties.getClientName()) //.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) //.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .redirectUri((StringUtils.hasText(oauthProperties.getServerUrl())?oauthProperties.getServerUrl():"{baseUrl}")+"/login/oauth2/code/{registrationId}") .scope(scopes) .userInfoUri(oauthProperties.getIssuerUri()+"/client/userinfo") .userNameAttributeName(IdTokenClaimNames.SUB) .build(); }}springsecurity对于oauth2.0的反对还是比较完善的,然而不完全符合咱们对认证受权零碎的要求,所以我这里做了很多自定义,能够说把大部分的拦截器都实现了一些,发现security的代码品质也不是很高,很多中央没有做到扩展性,只能重写类。 ...

November 24, 2021 · 2 min · jiezi

关于springsecurity:开发SpringBootJwtVue的前后端分离后台管理系统VueAdmin-后端笔记

为了让更多同学学习到前后端拆散管理系统的搭建过程,这里我写了具体的开发过程的文档,应用的是springsecurity + jwt + vue的技术栈组合,如果有帮忙,别忘了点个赞和关注我的公众号哈! 线上预览:https://markerhub.com/vueadmin 效果图: 首发公众号:MarkerHub 作者:吕一明 我的项目源码:关注公众号 MarkerHub 回复【 234 】获取 线上预览:https://markerhub.com/vueadmin 我的项目视频:https://www.bilibili.com/video/BV1af4y1s7Wh/ 转载请保留此申明,感激! 另外我还有另外一个前后端博客我的项目博客VueBlog,如果有须要能够关注公众号MarkerHub,回复【VueBlog】获取哈!! 1. 前言从零开始搭建一个我的项目骨架,最好抉择适合相熟的技术,并且在将来易拓展,适宜微服务化体系等。所以个别以Springboot作为咱们的框架根底,这是离不开的了。 而后数据层,咱们罕用的是Mybatis,易上手,不便保护。然而单表操作比拟艰难,特地是增加字段或缩小字段的时候,比拟繁琐,所以这里我举荐应用Mybatis Plus(https://mp.baomidou.com/),... CRUD 操作,从而节俭大量工夫。 作为一个我的项目骨架,权限也是咱们不能疏忽的,上一个我的项目vueblog咱们应用了shiro,然而有些同学想学学SpringSecurity,所以这一期咱们应用security作为咱们的权限管制和会话管制的框架。 思考到我的项目可能须要部署多台,一些须要共享的信息就保留在中间件中,Redis是当初支流的缓存中间件,也适宜咱们的我的项目。 而后因为前后端拆散,所以咱们应用jwt作为咱们用户身份凭证,并且session咱们会禁用,这样以前传统我的项目应用的形式咱们可能就不再适宜应用,这点须要留神了。 ok,咱们当初就开始搭建咱们的我的项目脚手架! 技术栈: SpringBootmybatis plusspring securitylombokredishibernate validatiorjwt2. 新建springboot我的项目,留神版本这里,咱们应用IDEA来开发咱们我的项目,新建步骤比较简单,咱们就不截图了。 开发工具与环境: ideamysqljdk 8maven3.3.9新建好的我的项目构造如下,SpringBoot版本应用的目前最新的2.4.0版本 pom的jar包导入如下: <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.4.0</version> <relativePath/></parent><groupId>com.markerhub</groupId><artifactId>vueadmin-java</artifactId><version>0.0.1-SNAPSHOT</version><name>vueadmin-java</name><description>公众号:MarkerHub</description><properties> <java.version>1.8</java.version></properties><dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency></dependencies>devtools:我的项目的热加载重启插件lombok:简化代码的工具3. 整合mybatis plus,生成代码接下来,咱们来整合mybatis plus,让我的项目能实现根本的增删改查操作。步骤很简略:能够去官网看看:https://mp.baomidou.com/guide/ 第一步:导入jar包pom中导入mybatis plus的jar包,因为前面会波及到代码生成,所以咱们还须要导入页面模板引擎,这里咱们用的是freemarker。 <!--整合mybatis plus https://baomidou.com/--><dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.1</version></dependency><!--mp代码生成器--><dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.4.1</version></dependency><dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.30</version></dependency><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope></dependency>第二步:而后去写配置文件server: port: 8081# DataSource Configspring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/vueadmin?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: adminmybatis-plus: mapper-locations: classpath*:/mapper/**Mapper.xml下面除了配置数据库的信息,还配置了myabtis plus的mapper的xml文件的扫描门路,这一步不要遗记了。而后因为前段默认是8080端口了,所以后端咱们设置为8081端口,避免端口抵触。 ...

April 29, 2021 · 14 min · jiezi

关于springboot:一security之环境要求和数据库配置说明

一、环境要求: jdk8 、装置了且配置了maven 、装置了msql 二、我的项目代码 1、导入依赖<dependency> <groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency> emm ,导入依赖后间接启动拜访接口会弹出一个表单要你输出,用户名为:user 明码为:在控制台看 2、创立数据库员工信息库CREATE TABLE tb_sys_employee (id varchar(20) NOT NULL COMMENT '员工id',name varchar(100) NOT NULL COMMENT '实在姓名',phone varchar(50) NOT NULL COMMENT '电话号码',card varchar(20) NOT NULL COMMENT '身份证号码',password varchar(40) NOT NULL COMMENT '明码(密文)',code varchar(64) NOT NULL COMMENT '工号',status tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态0 停用 1:启用',nick_name varchar(100) NOT NULL COMMENT '工号名称',salt varchar(8) NOT NULL COMMENT '盐',address varchar(64) DEFAULT NULL COMMENT '住址',create_name varchar(64) DEFAULT NULL COMMENT '创建人',create_time datetime DEFAULT NULL COMMENT '创立工夫',update_time datetime DEFAULT NULL COMMENT '更新工夫',role int(6) DEFAULT '1' COMMENT '1 admin 2、employee 4、user',avatar varchar(256) DEFAULT NULL COMMENT '头像', PRIMARY KEY (id) USING BTREE) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='员工表'; ...

April 23, 2021 · 2 min · jiezi

关于springsecurity:Spring-Security-之学习路途

Spring Security 学习之旅开始SpringSecurity 开始我的项目:Github 1. 引入依赖 <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.4.1</version> <relativePath/> </parent> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>在manven依赖仓库中: 2. 配置Security1.在包下创立SecurityCconfig类,重写configure办法,其中WebSecurity web,能够定义疏忽门路 @Override public void configure(WebSecurity web) throws Exception { //疏忽拦挡 web.ignoring().antMatchers("/sayHello","/doLogin"); }HttpSecurity http 能够拦挡申请,能够定义登录、登出等等 @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests()//开启登录 //示意拜访,ex/index 这个接口,须要具备admin角色 .antMatchers("/es/**").hasRole("admin") //示意残余的其余接口,登录之后能拜访 .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") //登录解决接口 .loginProcessingUrl("/doLogin") //定义登录时,用户名的key,默认为username .usernameParameter("username") //定义登录时,用户明码的key,默认为password .passwordParameter("password") //定义登录胜利的处理器 .successHandler(new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { response.setContentType("application/json;charset=utf-8"); response.sendRedirect("/success.html");//重定向到一个页面 MyUserDetails detail= (MyUserDetails)authentication.getPrincipal(); System.out.println(detail); } }) .failureHandler(new AuthenticationFailureHandler() { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { response.setContentType("application/json;charset=utf-8"); PrintWriter out = response.getWriter(); ResponseBean responseBean = ResponseBean.sendByCode("you have login failure !", 401); String result = new ObjectMapper().writeValueAsString(responseBean); out.write(result); out.flush(); } }) //和表单登录相干的接口通通都间接通过 .permitAll() .and() .logout() .logoutUrl("/logout") .logoutSuccessHandler(new LogoutSuccessHandler() { @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { response.setContentType("application/json;charset=utf-8"); PrintWriter out = response.getWriter(); out.write("you have login out success !"); out.flush(); } }) .permitAll() .and() .httpBasic() .and() .csrf().disable(); }简略的表单登录配置,这里的logou是Get申请,若要Post申请,则减少一行 ...

December 29, 2020 · 3 min · jiezi

Spring-Security-实战干货玩转自定义登录

1. 前言前面的关于 Spring Security 相关的文章只是一个预热。为了接下来更好的实战,如果你错过了请从 Spring Security 实战系列 开始。安全访问的第一步就是认证(Authentication),认证的第一步就是登录。今天我们要通过对 Spring Security 的自定义,来设计一个可扩展,可伸缩的 form 登录功能。 2. form 登录的流程下面是 form 登录的基本流程: 只要是 form 登录基本都能转化为上面的流程。接下来我们看看 Spring Security 是如何处理的。 3. Spring Security 中的登录昨天 Spring Security 实战干货:自定义配置类入口WebSecurityConfigurerAdapter 中已经讲到了我们通常的自定义访问控制主要是通过 HttpSecurity 来构建的。默认它提供了三种登录方式: formLogin() 普通表单登录oauth2Login() 基于 OAuth2.0 认证/授权协议openidLogin() 基于 OpenID 身份认证规范以上三种方式统统是 AbstractAuthenticationFilterConfigurer 实现的, 4. HttpSecurity 中的 form 表单登录启用表单登录通过两种方式一种是通过 HttpSecurity 的 apply(C configurer) 方法自己构造一个 AbstractAuthenticationFilterConfigurer 的实现,这种是比较高级的玩法。 另一种是我们常见的使用 HttpSecurity 的 formLogin() 方法来自定义 FormLoginConfigurer 。我们先搞一下比较常规的第二种。 ...

October 18, 2019 · 4 min · jiezi

Spring-Security-实战干货路径Uri中的-Ant-风格

1. 前言我们经常在读到一些文章会遇到uri 支持 Ant 风格 ,而且这个东西在 Spring MVC 和 Spring Security 中经常被提及。这到底是什么呢?今天我们来学习了解一下。这对我们学习 Spring MVC 和 Spring Security 十分必要。 2. Ant 风格说白了 Ant 风格就是一种路径匹配表达式。主要用来对uri的匹配。其实跟正则表达式作用是一样的,只不过正则表达式适用面更加宽泛,Ant仅仅用于路径匹配。 3. Ant 通配符Ant 中的通配符有三种: ? 匹配任何单字符* 匹配0或者任意数量的 字符** 匹配0或者更多的 目录这里注意了单个* 是在一个目录内进行匹配。 而** 是可以匹配多个目录,一定不要迷糊。 3.1 Ant 通配符示例通配符示例说明?/ant/p?ttern匹配项目根路径下 /ant/pattern 和 /ant/pXttern,但是不包括/ant/pttern*/ant/*.html匹配项目根路径下所有在ant路径下的.html文件*/ant/*/path/ant/path、/ant/a/path、/ant/bxx/path 都匹配,不匹配 /ant/axx/bxx/path**/ant/**/path/ant/path、/ant/a/path、/ant/bxx/path 、/ant/axx/bxx/path都匹配3.2 最长匹配原则从 3.1 可以看出 * 和 ** 是有冲突的情况存在的。为了解决这种冲突就规定了最长匹配原则(has more characters)。 一旦一个uri 同时符合两个Ant匹配那么走匹配规则字符最多的。为什么走最长?因为字符越长信息越多就越具体。比如 /ant/a/path 同时满足 /**/path 和 /ant/*/path 那么走/ant/*/path 4. Spring MVC 和 Spring Security 中的 Ant 风格接下来我们来看看 Spring MVC 和 Spring Security 下的 Ant风格。 ...

October 15, 2019 · 1 min · jiezi

There-is-no-PasswordEncoder-mapped-for-the-id-null

spring-boot 1.5.3 升级到 2.1.7 出现上述错误,查看MAVEN引用信息,引用的spring security版本为5.1.16,其官方文档地址为:https://docs.spring.io/spring... 原理猜想报错的代码在这: package org.springframework.security.crypto.password;public class DelegatingPasswordEncoder implements PasswordEncoder { @Override public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) { String id = extractId(prefixEncodedPassword); throw new IllegalArgumentException("There is no PasswordEncoder mapped for the id \"" + id + "\""); }}根据异常排查,大概的思想是这样: 获取密码获取默认加密、密码匹配对象1.1 获取获取的加密类型(加密前缀)1.2 根据类型找算法1.3 没找到算法,则调用默认算法1.4 默认算法代码如上,抛出异常: 第1.1步我给几个例子,帮助学习: 由密码{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG获取到的加密类型为bcrypt.由密码{noop}password获取到加密类开地为noop由密码{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0中获取到的加密类型为sha256。具体的出错的逻辑是这样的: 获取到了密码,比如为123456.获取默认加密、密码匹配对象:DelegatingPasswordEncoder1.1 spring尝试从123456中,获取一个加密前缀的东西。但获取的值为null。1.2 没有找到算法,则调用默认算法,此时默认对象为:UnmappedIdPasswordEncoder1.3 运行对象UnmappedIdPasswordEncoder的matches算法1.4 抛出异常。 解决问题spring security支持的列表如下: String encodingId = "bcrypt"; Map<String, PasswordEncoder> encoders = new HashMap<>(); encoders.put(encodingId, new BCryptPasswordEncoder()); encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder()); encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder()); encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5")); encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance()); encoders.put("pbkdf2", new Pbkdf2PasswordEncoder()); encoders.put("scrypt", new SCryptPasswordEncoder()); encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1")); encoders.put("SHA-256", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256")); encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());使用了预制算法可以将数据库中密码更新为{算法前缀}原密码,来进行升级。新增数据时,密码字段也要加入前缀。 ...

August 17, 2019 · 2 min · jiezi

【SpringSecurity系列02】SpringSecurity 表单认证逻辑源码解读

概要前面一节,通过简单配置即可实现SpringSecurity表单认证功能,而今天这一节将通过阅读源码的形式来学习SpringSecurity是如何实现这些功能, 前方高能预警,本篇分析源码篇幅较长。<!– more –>过滤器链前面我说过SpringSecurity是基于过滤器链的形式,那么我解析将会介绍一下具体有哪些过滤器。Filter Class介绍SecurityContextPersistenceFilter判断当前用户是否登录CrsfFilter用于防止csrf攻击LogoutFilter处理注销请求UsernamePasswordAuthenticationFilter处理表单登录的请求(也是我们今天的主角)BasicAuthenticationFilter处理http basic认证的请求由于过滤器链中的过滤器实在太多,我没有一一列举,调了几个比较重要的介绍一下。通过上面我们知道SpringSecurity对于表单登录的认证请求是交给了UsernamePasswordAuthenticationFilter处理的,那么具体的认证流程如下:从上图可知,UsernamePasswordAuthenticationFilter继承于抽象类AbstractAuthenticationProcessingFilter。具体认证是:进入doFilter方法,判断是否要认证,如果需要认证则进入attemptAuthentication方法,如果不需要直接结束attemptAuthentication方法中根据username跟password构造一个UsernamePasswordAuthenticationToken对象(此时的token是未认证的),并且将它交给ProviderManger来完成认证。ProviderManger中维护这一个AuthenticationProvider对象列表,通过遍历判断并且最后选择DaoAuthenticationProvider对象来完成最后的认证。DaoAuthenticationProvider根据ProviderManger传来的token取出username,并且调用我们写的UserDetailsService的loadUserByUsername方法从数据库中读取用户信息,然后对比用户密码,如果认证通过,则返回用户信息也是就是UserDetails对象,在重新构造UsernamePasswordAuthenticationToken(此时的token是 已经认证通过了的)。接下来我们将通过源码来分析具体的整个认证流程。AbstractAuthenticationProcessingFilterAbstractAuthenticationProcessingFilter 是一个抽象类。所有的认证认证请求的过滤器都会继承于它,它主要将一些公共的功能实现,而具体的验证逻辑交给子类实现,有点类似于父类设置好认证流程,子类负责具体的认证逻辑,这样跟设计模式的模板方法模式有点相似。现在我们分析一下 它里面比较重要的方法1、doFilterpublic void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { // 省略不相干代码。。。 // 1、判断当前请求是否要认证 if (!requiresAuthentication(request, response)) { // 不需要直接走下一个过滤器 chain.doFilter(request, response); return; } try { // 2、开始请求认证,attemptAuthentication具体实现给子类,如果认证成功返回一个认证通过的Authenticaion对象 authResult = attemptAuthentication(request, response); if (authResult == null) { return; } // 3、登录成功 将认证成功的用户信息放入session SessionAuthenticationStrategy接口,用于扩展 sessionStrategy.onAuthentication(authResult, request, response); } catch (InternalAuthenticationServiceException failed) { //2.1、发生异常,登录失败,进入登录失败handler回调 unsuccessfulAuthentication(request, response, failed); return; } catch (AuthenticationException failed) { //2.1、发生异常,登录失败,进入登录失败处理器 unsuccessfulAuthentication(request, response, failed); return; } // 3.1、登录成功,进入登录成功处理器。 successfulAuthentication(request, response, chain, authResult); }2、successfulAuthentication登录成功处理器protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { //1、登录成功 将认证成功的Authentication对象存入SecurityContextHolder中 // SecurityContextHolder本质是一个ThreadLocal SecurityContextHolder.getContext().setAuthentication(authResult); //2、如果开启了记住我功能,将调用rememberMeServices的loginSuccess 将生成一个token // 将token放入cookie中这样 下次就不用登录就可以认证。具体关于记住我rememberMeServices的相关分析我 们下面几篇文章会深入分析的。 rememberMeServices.loginSuccess(request, response, authResult); // Fire event //3、发布一个登录事件。 if (this.eventPublisher != null) { eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent( authResult, this.getClass())); } //4、调用我们自己定义的登录成功处理器,这样也是我们扩展得知登录成功的一个扩展点。 successHandler.onAuthenticationSuccess(request, response, authResult); }3、unsuccessfulAuthentication登录失败处理器protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { //1、登录失败,将SecurityContextHolder中的信息清空 SecurityContextHolder.clearContext(); //2、关于记住我功能的登录失败处理 rememberMeServices.loginFail(request, response); //3、调用我们自己定义的登录失败处理器,这里可以扩展记录登录失败的日志。 failureHandler.onAuthenticationFailure(request, response, failed); }关于AbstractAuthenticationProcessingFilter主要分析就到这。我们可以从源码中知道,当请求进入该过滤器中具体的流程是判断该请求是否要被认证调用attemptAuthentication方法开始认证,由于是抽象方法具体认证逻辑给子类如果登录成功,则将认证结果Authentication对象根据session策略写入session中,将认证结果写入到SecurityContextHolder,如果开启了记住我功能,则根据记住我功能,生成token并且写入cookie中,最后调用一个successHandler对象的方法,这个对象可以是我们配置注入的,用于处理我们的自定义登录成功的一些逻辑(比如记录登录成功日志等等)。如果登录失败,则清空SecurityContextHolder中的信息,并且调用我们自己注入的failureHandler对象,处理我们自己的登录失败逻辑。UsernamePasswordAuthenticationFilter从上面分析我们可以知道,UsernamePasswordAuthenticationFilter是继承于AbstractAuthenticationProcessingFilter,并且实现它的attemptAuthentication方法,来实现认证具体的逻辑实现。接下来,我们通过阅读UsernamePasswordAuthenticationFilter的源码来解读,它是如何完成认证的。 由于这里会涉及UsernamePasswordAuthenticationToken对象构造,所以我们先看看UsernamePasswordAuthenticationToken的源码1、UsernamePasswordAuthenticationToken// 继承至AbstractAuthenticationToken // AbstractAuthenticationToken主要定义一下在SpringSecurity中toke需要存在一些必须信息// 例如权限集合 Collection<GrantedAuthority> authorities; 是否认证通过boolean authenticated = false;认证通过的用户信息Object details;public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken { // 未登录情况下 存的是用户名 登录成功情况下存的是UserDetails对象 private final Object principal; // 密码 private Object credentials; /** * 构造函数,用户没有登录的情况下,此时的authenticated是false,代表尚未认证 / public UsernamePasswordAuthenticationToken(Object principal, Object credentials) { super(null); this.principal = principal; this.credentials = credentials; setAuthenticated(false); } /* * 构造函数,用户登录成功的情况下,多了一个参数 是用户的权限集合,此时的authenticated是true,代表认证成功 / public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; this.credentials = credentials; super.setAuthenticated(true); // must use super, as we override }}接下来我们就可以分析attemptAuthentication方法了。2、attemptAuthenticationpublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { // 1、判断是不是post请求,如果不是则抛出AuthenticationServiceException异常,注意这里抛出的异常都在AbstractAuthenticationProcessingFilter#doFilter方法中捕获,捕获之后会进入登录失败的逻辑。 if (postOnly && !request.getMethod().equals(“POST”)) { throw new AuthenticationServiceException( “Authentication method not supported: " + request.getMethod()); } // 2、从request中拿用户名跟密码 String username = obtainUsername(request); String password = obtainPassword(request); // 3、非空处理,防止NPE异常 if (username == null) { username = “”; } if (password == null) { password = “”; } // 4、除去空格 username = username.trim(); // 5、根据username跟password构造出一个UsernamePasswordAuthenticationToken对象 从上文分析可知道,此时的token是未认证的。 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); // 6、配置一下其他信息 ip 等等 setDetails(request, authRequest); // 7、调用ProviderManger的authenticate的方法进行具体认证逻辑 return this.getAuthenticationManager().authenticate(authRequest); }ProviderManager维护一个AuthenticationProvider列表,进行认证逻辑验证1、authenticatepublic Authentication authenticate(Authentication authentication) throws AuthenticationException { // 1、拿到token的类型。 Class<? extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null; Authentication result = null; // 2、遍历AuthenticationProvider列表 for (AuthenticationProvider provider : getProviders()) { // 3、AuthenticationProvider不支持当前token类型,则直接跳过 if (!provider.supports(toTest)) { continue; } try { // 4、如果Provider支持当前token,则交给Provider完成认证。 result = provider.authenticate(authentication); } catch (AccountStatusException e) { throw e; } catch (InternalAuthenticationServiceException e) { throw e; } catch (AuthenticationException e) { lastException = e; } } // 5、登录成功 返回登录成功的token if (result != null) { eventPublisher.publishAuthenticationSuccess(result); return result; } }AbstractUserDetailsAuthenticationProvider1、authenticateAbstractUserDetailsAuthenticationProvider实现了AuthenticationProvider接口,并且实现了部分方法,DaoAuthenticationProvider继承于AbstractUserDetailsAuthenticationProvider类,所以我们先来看看AbstractUserDetailsAuthenticationProvider的实现。public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware { // 国际化处理 protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); /* * 对token一些检查,具体检查逻辑交给子类实现,抽象方法 / protected abstract void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException; /* * 认证逻辑的实现,调用抽象方法retrieveUser根据username获取UserDetails对象 */ public Authentication authenticate(Authentication authentication) throws AuthenticationException { // 1、获取usernmae String username = (authentication.getPrincipal() == null) ? “NONE_PROVIDED” : authentication.getName(); // 2、尝试去缓存中获取UserDetails对象 UserDetails user = this.userCache.getUserFromCache(username); // 3、如果为空,则代表当前对象没有缓存。 if (user == null) { cacheWasUsed = false; try { //4、调用retrieveUser去获取UserDetail对象,为什么这个方法是抽象方法大家很容易知道,如果UserDetail信息存在关系数据库 则可以重写该方法并且去关系数据库获取用户信息,如果UserDetail信息存在其他地方,可以重写该方法用其他的方法去获取用户信息,这样丝毫不影响整个认证流程,方便扩展。 user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException notFound) { // 捕获异常 日志处理 并且往上抛出,登录失败。 if (hideUserNotFoundExceptions) { throw new BadCredentialsException(messages.getMessage( “AbstractUserDetailsAuthenticationProvider.badCredentials”, “Bad credentials”)); } else { throw notFound; } } } try { // 5、前置检查 判断当前用户是否锁定,禁用等等 preAuthenticationChecks.check(user); // 6、其他的检查,在DaoAuthenticationProvider是检查密码是否一致 additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } catch (AuthenticationException exception) { } // 7、后置检查,判断密码是否过期 postAuthenticationChecks.check(user); // 8、登录成功通过UserDetail对象重新构造一个认证通过的Token对象 return createSuccessAuthentication(principalToReturn, authentication, user); } protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) { // 调用第二个构造方法,构造一个认证通过的Token对象 UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken( principal, authentication.getCredentials(), authoritiesMapper.mapAuthorities(user.getAuthorities())); result.setDetails(authentication.getDetails()); return result; }}接下来我们具体看看retrieveUser的实现,没看源码大家应该也可以知道,retrieveUser方法应该是调用UserDetailsService去数据库查询是否有该用户,以及用户的密码是否一致。DaoAuthenticationProviderDaoAuthenticationProvider 主要是通过UserDetailService来获取UserDetail对象。1、retrieveUserprotected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { try { // 1、调用UserDetailsService接口的loadUserByUsername方法获取UserDeail对象 UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); // 2、如果loadedUser为null 代表当前用户不存在,抛出异常 登录失败。 if (loadedUser == null) { throw new InternalAuthenticationServiceException( “UserDetailsService returned null, which is an interface contract violation”); } // 3、返回查询的结果 return loadedUser; } }2、additionalAuthenticationChecksprotected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { // 1、如果密码为空,则抛出异常、 if (authentication.getCredentials() == null) { throw new BadCredentialsException(messages.getMessage( “AbstractUserDetailsAuthenticationProvider.badCredentials”, “Bad credentials”)); } // 2、获取用户输入的密码 String presentedPassword = authentication.getCredentials().toString(); // 3、调用passwordEncoder的matche方法 判断密码是否一致 if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { logger.debug(“Authentication failed: password does not match stored value”); // 4、如果不一致 则抛出异常。 throw new BadCredentialsException(messages.getMessage( “AbstractUserDetailsAuthenticationProvider.badCredentials”, “Bad credentials”)); } }总结至此,整认证流程已经分析完毕,大家如果有什么不懂可以关注我的公众号一起讨论。学习是一个漫长的过程,学习源码可能会很困难但是只要努力一定就会有获取,大家一致共勉。 ...

April 16, 2019 · 4 min · jiezi

【SpringSecurity系列01】初识SpringSecurity

什么是SpringSecurity ? Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。以上来介绍来自wiki,比较官方。 用自己的话 简单介绍一下,Spring Security 基于 Servlet 过滤器链的形式,为我们的web项目提供认证与授权服务。它来自于Spring,那么它与SpringBoot整合开发有着天然的优势,目前与SpringSecurity对应的开源框架还有shiro。接下来我将通过一个简单的例子带大家来认识SpringSecurity,然后通过分析它的源码带大家来认识一下SpringSecurity是如何工作,从一个简单例子入门,大家由浅入深的了解学习SpringSecurity。通常大家在做一个后台管理的系统的时候,应该采用session判断用户是否登录。我记得我在没有接触学习SpringSecurity与shiro之前。对于用户登录功能实现通常是如下:public String login(User user, HttpSession session){ //1、根据用户名或者id从数据库读取数据库中用户 //2、判断密码是否一致 //3、如果密码一致 session.setAttribute(“user”,user); //4、否则返回登录页面 }对于之后那些需要登录之后才能访问的url,通过SpringMvc的拦截器中的#preHandle来判断session中是否有user对象如果没有 则返回登录页面如果有, 则允许访问这个页面。但是在SpringSecurity中,这一些逻辑已经被封装起来,我们只需要简单的配置一下就能使用。接下来我通过一个简单例子大家认识一下SpringSecurity本文基于SpringBoot,如果大家对SpringBoot不熟悉的话可以看看我之前写的SpringBoot入门系列我使用的是:SpringBoot 2.1.4.RELEASESpringSecurity 5<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.4.RELEASE</version> <relativePath/> <!– lookup parent from repository –> </parent> <groupId>com.yukong</groupId> <artifactId>springboot-springsecurity</artifactId> <version>0.0.1-SNAPSHOT</version> <name>springboot-springsecurity</name> <description>springboot-springsecurity study</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.0.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>配置一下数据库 以及MyBatisspring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/db_test?useUnicode=true&characterEncoding=utf8 password: abc123mybatis: mapper-locations: classpath:mapper/*.xml这里我用的MySQL8.0 大家注意一下 MySQL8.0的数据库驱动的类的包改名了在前面我有讲过SpringBoot中如何整合Mybatis,在这里我就不累述,有需要的话看这篇文章user.sqlCREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT ‘主键’, username varchar(32) NOT NULL COMMENT ‘用户名’, svc_num varchar(32) DEFAULT NULL COMMENT ‘用户号码’, password varchar(100) DEFAULT NULL COMMENT ‘密码’, cust_id bigint(20) DEFAULT NULL COMMENT ‘客户id 1对1’, PRIMARY KEY (id)) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;对应的UserMapper.javapackage com.yukong.mapper;import com.yukong.entity.User;/** * * @author yukong * @date 2019-04-11 16:50 /public interface UserMapper { int insertSelective(User record); User selectByUsername(String username);}UserMapper.xml<?xml version=“1.0” encoding=“UTF-8”?><!DOCTYPE mapper PUBLIC “-//mybatis.org//DTD Mapper 3.0//EN” “http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace=“com.yukong.mapper.UserMapper”> <resultMap id=“BaseResultMap” type=“com.yukong.entity.User”> <id column=“id” jdbcType=“BIGINT” property=“id” /> <result column=“username” jdbcType=“VARCHAR” property=“username” /> <result column=“svc_num” jdbcType=“VARCHAR” property=“svcNum” /> <result column=“password” jdbcType=“VARCHAR” property=“password” /> <result column=“cust_id” jdbcType=“BIGINT” property=“custId” /> </resultMap> <sql id=“Base_Column_List”> id, username, svc_num, password, cust_id </sql> <select id=“selectByUsername” parameterType=“java.lang.String” resultMap=“BaseResultMap”> select <include refid=“Base_Column_List” /> from user where username = #{username,jdbcType=VARCHAR} </select> <insert id=“insertSelective” keyColumn=“id” keyProperty=“id” parameterType=“com.yukong.entity.User” useGeneratedKeys=“true”> insert into user <trim prefix=”(” suffix=”)” suffixOverrides=","> <if test=“username != null”> username, </if> <if test=“svcNum != null”> svc_num, </if> <if test=“password != null”> password, </if> <if test=“custId != null”> cust_id, </if> </trim> <trim prefix=“values (” suffix=")" suffixOverrides=","> <if test=“username != null”> #{username,jdbcType=VARCHAR}, </if> <if test=“svcNum != null”> #{svcNum,jdbcType=VARCHAR}, </if> <if test=“password != null”> #{password,jdbcType=VARCHAR}, </if> <if test=“custId != null”> #{custId,jdbcType=BIGINT}, </if> </trim> </insert></mapper>在这里我们定义了两个方法。国际惯例ctrl+shift+t创建mapper的测试方法,并且插入一条记录package com.yukong.mapper;import com.yukong.SpringbootSpringsecurityApplicationTests;import com.yukong.entity.User;import org.junit.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.crypto.password.PasswordEncoder;import static org.junit.Assert.;/** * @author yukong * @date 2019-04-11 16:53 /public class UserMapperTest extends SpringbootSpringsecurityApplicationTests { @Autowired private UserMapper userMapper; @Test public void insert() { User user = new User(); user.setUsername(“yukong”); user.setPassword(“abc123”); userMapper.insertSelective(user); }}运行测试方法,并且成功插入一条记录。创建UserController.javapackage com.yukong.controller;import com.yukong.entity.User;import com.yukong.mapper.UserMapper;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;/* * @author yukong * @date 2019-04-11 15:22 /@RestControllerpublic class UserController { @Autowired private UserMapper userMapper; @RequestMapping("/user/{username}") public User hello(@PathVariable String username) { return userMapper.selectByUsername(username); }}这个方法就是根据用户名去数据库查找用户详细信息。启动。因为我们之前插入过一条username=yukong的记录,所以我们查询一下,访问127.0.0.1:8080/user/yukong[图片上传失败…(image-ea02ac-1554981869345)]我们可以看到 我们被重定向到了一个登录界面,这也是我们之前引入的spring-boot-security-starter起作用了。大家可能想问了,用户名跟密码是什么,用户名默认是user,密码在启动的时候已经通过日志打印在控制台了。现在我们输入用户跟密码并且登录。就可以成功访问我们想要访问的接口。从这里我们可以知道,我只需要引入了Spring-Security的依赖,它就开始生效,并且保护我们的接口了,但是现在有一个问题就是,它的用户名只能是user并且密码是通过日志打印在控制台,但是我们希望它能通过数据来访问我们的用户并且判断登录。其实想实现这个功能也很简单。这里我们需要了解两个接口。UserDetailsUserDetailsService所以,我们需要将我们的User.java实现这个接口package com.yukong.entity;import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.authority.AuthorityUtils;import org.springframework.security.core.userdetails.UserDetails;import java.util.Collection;/* * * @author yukong * @date 2019-04-11 16:50 /public class User implements UserDetails { /* * 主键 / private Long id; /* * 用户名 / private String username; /* * 用户号码 / private String svcNum; /* * 密码 / private String password; /* * 客户id 1对1 / private Long custId; public Long getId() { return id; } public void setId(Long id) { this.id = id; } @Override public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { return false; } @Override public boolean isAccountNonLocked() { return false; } @Override public boolean isCredentialsNonExpired() { return false; } @Override public boolean isEnabled() { return false; } public void setUsername(String username) { this.username = username; } public String getSvcNum() { return svcNum; } public void setSvcNum(String svcNum) { this.svcNum = svcNum; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { // 这里我们没有用到权限,所以返回一个默认的admin权限 return AuthorityUtils.commaSeparatedStringToAuthorityList(“admin”); } @Override public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public Long getCustId() { return custId; } public void setCustId(Long custId) { this.custId = custId; }}接下来我们再看看UserDetailsService它只有一个方法的声明,就是通过用户名去查找用户信息,从这里我们应该知道了,SpringSecurity回调UserDetails#loadUserByUsername去获取用户,但是它不知道用户信息存在哪里,所以定义成接口,让使用者去实现。在我们这个项目用 我们的用户是存在了数据库中,所以我们需要调用UserMapper的方法去访问数据库查询用户信息。这里我们新建一个类叫MyUserDetailsServiceImplpackage com.yukong.config;import com.yukong.mapper.UserMapper;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.security.core.userdetails.UsernameNotFoundException;import org.springframework.stereotype.Service;/* * @author yukong * @date 2019-04-11 17:35 /@Servicepublic class MyUserDetailServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { return userMapper.selectByUsername(username); }}然后新建一个类去把我们的UserDetailsService配置进去这里我们新建一个SecurityConfigpackage com.yukong.config;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;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.WebSecurityConfigurerAdapter;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.crypto.password.PasswordEncoder;/* * @author yukong * @date 2019-04-11 15:08 /@Configurationpublic class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { // 配置UserDetailsService 跟 PasswordEncoder 加密器 auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); auth.eraseCredentials(false); }}在这里我们还配置了一个PasswordEncoder加密我们的密码,大家都知道密码明文存数据库是很不安全的。接下里我们插入一条记录,需要注意的是 密码需要加密。package com.yukong.mapper;import com.yukong.SpringbootSpringsecurityApplicationTests;import com.yukong.entity.User;import org.junit.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.crypto.password.PasswordEncoder;import static org.junit.Assert.;/** * @author yukong * @date 2019-04-11 16:53 */public class UserMapperTest extends SpringbootSpringsecurityApplicationTests { @Autowired private PasswordEncoder passwordEncoder; @Autowired private UserMapper userMapper; @Test public void insert() { User user = new User(); user.setUsername(“yukong”); user.setPassword(passwordEncoder.encode(“abc123”)); userMapper.insertSelective(user); }}接下来启动程序,并且登录,这次只需要输入插入到数据中的那条记录的用户名跟密码即可。在这里一节中,我们了解到如何使用springsecurity 完成一个登录功能,接下我们将通过分析源码来了解为什么需要这个配置,以及SpringSecurity的工作原理是什么。 ...

April 11, 2019 · 4 min · jiezi

干货|一个案例学会Spring Security 中使用 JWT

在前后端分离的项目中,登录策略也有不少,不过 JWT 算是目前比较流行的一种解决方案了,本文就和大家来分享一下如何将 Spring Security 和 JWT 结合在一起使用,进而实现前后端分离时的登录解决方案。1 无状态登录1.1 什么是有状态?有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如Tomcat中的Session。例如登录:用户登录后,我们把用户的信息保存在服务端session中,并且给用户一个cookie值,记录对应的session,然后下次请求,用户携带cookie值来(这一步有浏览器自动完成),我们就能识别到对应session,从而找到用户的信息。这种方式目前来看最方便,但是也有一些缺陷,如下:服务端保存大量数据,增加服务端压力服务端保存用户状态,不支持集群化部署1.2 什么是无状态微服务集群中的每个服务,对外提供的都使用RESTful风格的接口。而RESTful风格的一个最重要的规范就是:服务的无状态性,即:服务端不保存任何客户端请求者信息客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份那么这种无状态性有哪些好处呢?客户端请求不依赖服务端的信息,多次请求不需要必须访问到同一台服务器服务端的集群和状态对客户端透明服务端可以任意的迁移和伸缩(可以方便的进行集群化部署)减小服务端存储压力1.3.如何实现无状态无状态登录的流程:首先客户端发送账户名/密码到服务端进行认证认证通过后,服务端将用户信息加密并且编码成一个token,返回给客户端以后客户端每次发送请求,都需要携带认证的token服务端对客户端发送来的token进行解密,判断是否有效,并且获取用户登录信息1.4 JWT1.4.1 简介JWT,全称是Json Web Token, 是一种JSON风格的轻量级的授权和身份认证规范,可实现无状态、分布式的Web应用授权: JWT 作为一种规范,并没有和某一种语言绑定在一起,常用的Java 实现是GitHub 上的开源项目 jjwt,地址如下:https://github.com/jwtk/jjwt1.4.2 JWT数据格式JWT包含三部分数据:Header:头部,通常头部有两部分信息:声明类型,这里是JWT加密算法,自定义我们会对头部进行Base64Url编码(可解码),得到第一部分数据。Payload:载荷,就是有效数据,在官方文档中(RFC7519),这里给了7个示例信息:iss (issuer):表示签发人exp (expiration time):表示token过期时间sub (subject):主题aud (audience):受众nbf (Not Before):生效时间iat (Issued At):签发时间jti (JWT ID):编号这部分也会采用Base64Url编码,得到第二部分数据。Signature:签名,是整个数据的认证信息。一般根据前两步的数据,再加上服务的的密钥secret(密钥保存在服务端,不能泄露给客户端),通过Header中配置的加密算法生成。用于验证整个数据完整和可靠性。生成的数据格式如下图: 注意,这里的数据通过 . 隔开成了三部分,分别对应前面提到的三部分,另外,这里数据是不换行的,图片换行只是为了展示方便而已。1.4.3 JWT交互流程流程图:步骤翻译:应用程序或客户端向授权服务器请求授权获取到授权后,授权服务器会向应用程序返回访问令牌应用程序使用访问令牌来访问受保护资源(如API)因为JWT签发的token中已经包含了用户的身份信息,并且每次请求都会携带,这样服务的就无需保存用户信息,甚至无需去数据库查询,这样就完全符合了RESTful的无状态规范。1.5 JWT 存在的问题说了这么多,JWT 也不是天衣无缝,由客户端维护登录状态带来的一些问题在这里依然存在,举例如下:续签问题,这是被很多人诟病的问题之一,传统的cookie+session的方案天然的支持续签,但是jwt由于服务端不保存用户状态,因此很难完美解决续签问题,如果引入redis,虽然可以解决问题,但是jwt也变得不伦不类了。注销问题,由于服务端不再保存用户信息,所以一般可以通过修改secret来实现注销,服务端secret修改后,已经颁发的未过期的token就会认证失败,进而实现注销,不过毕竟没有传统的注销方便。密码重置,密码重置后,原本的token依然可以访问系统,这时候也需要强制修改secret。基于第2点和第3点,一般建议不同用户取不同secret。2 实战说了这么久,接下来我们就来看看这个东西到底要怎么用?2.1 环境搭建首先我们来创建一个Spring Boot项目,创建时需要添加Spring Security依赖,创建完成后,添加 jjwt 依赖,完整的pom.xml文件如下:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency><dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version></dependency>然后在项目中创建一个简单的 User 对象实现 UserDetails 接口,如下:public class User implements UserDetails { private String username; private String password; private List<GrantedAuthority> authorities; public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } //省略getter/setter}这个就是我们的用户对象,先放着备用,再创建一个HelloController,内容如下:@RestControllerpublic class HelloController { @GetMapping("/hello") public String hello() { return “hello jwt !”; } @GetMapping("/admin") public String admin() { return “hello admin !”; }}HelloController 很简单,这里有两个接口,设计是 /hello 接口可以被具有 user 角色的用户访问,而 /admin 接口则可以被具有 admin 角色的用户访问。2.2 JWT 过滤器配置接下来提供两个和 JWT 相关的过滤器配置:一个是用户登录的过滤器,在用户的登录的过滤器中校验用户是否登录成功,如果登录成功,则生成一个token返回给客户端,登录失败则给前端一个登录失败的提示。第二个过滤器则是当其他请求发送来,校验token的过滤器,如果校验成功,就让请求继续执行。这两个过滤器,我们分别来看,先看第一个:public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter { protected JwtLoginFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) { super(new AntPathRequestMatcher(defaultFilterProcessesUrl)); setAuthenticationManager(authenticationManager); } @Override public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse resp) throws AuthenticationException, IOException, ServletException { User user = new ObjectMapper().readValue(req.getInputStream(), User.class); return getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword())); } @Override protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse resp, FilterChain chain, Authentication authResult) throws IOException, ServletException { Collection<? extends GrantedAuthority> authorities = authResult.getAuthorities(); StringBuffer as = new StringBuffer(); for (GrantedAuthority authority : authorities) { as.append(authority.getAuthority()) .append(","); } String jwt = Jwts.builder() .claim(“authorities”, as)//配置用户角色 .setSubject(authResult.getName()) .setExpiration(new Date(System.currentTimeMillis() + 10 * 60 * 1000)) .signWith(SignatureAlgorithm.HS512,“sang@123”) .compact(); resp.setContentType(“application/json;charset=utf-8”); PrintWriter out = resp.getWriter(); out.write(new ObjectMapper().writeValueAsString(jwt)); out.flush(); out.close(); } protected void unsuccessfulAuthentication(HttpServletRequest req, HttpServletResponse resp, AuthenticationException failed) throws IOException, ServletException { resp.setContentType(“application/json;charset=utf-8”); PrintWriter out = resp.getWriter(); out.write(“登录失败!”); out.flush(); out.close(); }}关于这个类,我说如下几点:自定义 JwtLoginFilter 继承自 AbstractAuthenticationProcessingFilter,并实现其中的三个默认方法。attemptAuthentication方法中,我们从登录参数中提取出用户名密码,然后调用AuthenticationManager.authenticate()方法去进行自动校验。第二步如果校验成功,就会来到successfulAuthentication回调中,在successfulAuthentication方法中,将用户角色遍历然后用一个 , 连接起来,然后再利用Jwts去生成token,按照代码的顺序,生成过程一共配置了四个参数,分别是用户角色、主题、过期时间以及加密算法和密钥,然后将生成的token写出到客户端。第二步如果校验失败就会来到unsuccessfulAuthentication方法中,在这个方法中返回一个错误提示给客户端即可。再来看第二个token校验的过滤器:public class JwtFilter extends GenericFilterBean { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) servletRequest; String jwtToken = req.getHeader(“authorization”); System.out.println(jwtToken); Claims claims = Jwts.parser().setSigningKey(“sang@123”).parseClaimsJws(jwtToken.replace(“Bearer”,"")) .getBody(); String username = claims.getSubject();//获取当前登录用户名 List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList((String) claims.get(“authorities”)); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, authorities); SecurityContextHolder.getContext().setAuthentication(token); filterChain.doFilter(req,servletResponse); }}关于这个过滤器,我说如下几点:首先从请求头中提取出 authorization 字段,这个字段对应的value就是用户的token。将提取出来的token字符串转换为一个Claims对象,再从Claims对象中提取出当前用户名和用户角色,创建一个UsernamePasswordAuthenticationToken放到当前的Context中,然后执行过滤链使请求继续执行下去。如此之后,两个和JWT相关的过滤器就算配置好了。2.3 Spring Security 配置接下来我们来配置 Spring Security,如下:@Configurationpublic class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Bean PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication().withUser(“admin”) .password(“123”).roles(“admin”) .and() .withUser(“sang”) .password(“456”) .roles(“user”); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/hello").hasRole(“user”) .antMatchers("/admin").hasRole(“admin”) .antMatchers(HttpMethod.POST, “/login”).permitAll() .anyRequest().authenticated() .and() .addFilterBefore(new JwtLoginFilter("/login",authenticationManager()),UsernamePasswordAuthenticationFilter.class) .addFilterBefore(new JwtFilter(),UsernamePasswordAuthenticationFilter.class) .csrf().disable(); }}简单起见,这里我并未对密码进行加密,因此配置了NoOpPasswordEncoder的实例。简单起见,这里并未连接数据库,我直接在内存中配置了两个用户,两个用户具备不同的角色。配置路径规则时, /hello 接口必须要具备 user 角色才能访问, /admin 接口必须要具备 admin 角色才能访问,POST 请求并且是 /login 接口则可以直接通过,其他接口必须认证后才能访问。最后配置上两个自定义的过滤器并且关闭掉csrf保护。2.4 测试做完这些之后,我们的环境就算完全搭建起来了,接下来启动项目然后在 POSTMAN 中进行测试,如下: 登录成功后返回的字符串就是经过 base64url 转码的token,一共有三部分,通过一个 . 隔开,我们可以对第一个 . 之前的字符串进行解码,即Header,如下: 再对两个 . 之间的字符解码,即 payload: 可以看到,我们设置信息,由于base64并不是加密方案,只是一种编码方案,因此,不建议将敏感的用户信息放到token中。 接下来再去访问 /hello 接口,注意认证方式选择 Bearer Token,Token值为刚刚获取到的值,如下: 可以看到,访问成功。总结这就是 JWT 结合 Spring Security 的一个简单用法,讲真,如果实例允许,类似的需求我还是推荐使用 OAuth2 中的 password 模式。 不知道大伙有没有看懂呢?如果没看懂,松哥还有一个关于这个知识点的视频教程,如下: 如何获取这个视频教程呢?很简单,将本文转发到一个超过100人的微信群中(QQ群不算,松哥是群主的微信群也不算,群要为Java方向),或者多个微信群中,只要累计人数达到100人即可,然后加松哥微信,截图发给松哥即可获取资料。 ...

April 8, 2019 · 2 min · jiezi

Spring Boot Security OAuth2 实现支持JWT令牌的授权服务器

标题文字 #### 概要之前的两篇文章,讲述了Spring Security 结合 OAuth2 、JWT 的使用,这一节要求对 OAuth2、JWT 有了解,若不清楚,先移步到下面两篇提前了解下。Spring Boot Security 整合 OAuth2 设计安全API接口服务Spring Boot Security 整合 JWT 实现 无状态的分布式API接口这一篇我们来实现 支持 JWT令牌 的授权服务器。优点使用 OAuth2 是向认证服务器申请令牌,客户端拿这令牌访问资源服务服务器,资源服务器校验了令牌无误后,如果资源的访问用到用户的相关信息,那么资源服务器还需要根据令牌关联查询用户的信息。使用 JWT 是客户端通过用户名、密码 请求服务器获取 JWT,服务器判断用户名和密码无误之后,可以将用户信息和权限信息经过加密成 JWT 的形式返回给客户端。在之后的请求中,客户端携带 JWT 请求需要访问的资源,如果资源的访问用到用户的相关信息,那么就直接从JWT中获取到。所以,如果我们在使用 OAuth2 时结合JWT ,就能节省集中式令牌校验开销,实现无状态授权认证。快速上手项目说明工程名端口作用jwt-authserver8080授权服务器jwt-resourceserver8081资源服务器授权服务器pom.xml<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId></dependency><dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> <version>2.1.3.RELEASE</version></dependency><dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> <version>1.0.10.RELEASE</version></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency>WebSecurityConfig@Configurationpublic class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http. authorizeRequests().antMatchers("/").permitAll(); } @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser(“user”).password(“123456”).roles(“USER”); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Bean public PasswordEncoder passwordEncoder() { return new PasswordEncoder() { @Override public String encode(CharSequence charSequence) { return charSequence.toString(); } @Override public boolean matches(CharSequence charSequence, String s) { return Objects.equals(charSequence.toString(),s); } }; }}为了方便,使用内存模式,在内存中创建一个用户 user 密码 123456。OAuth2AuthorizationServer/ * 授权服务器 /@Configuration@EnableAuthorizationServerpublic class OAuth2AuthorizationServer extends AuthorizationServerConfigurerAdapter { /* * 注入AuthenticationManager ,密码模式用到 / @Autowired private AuthenticationManager authenticationManager; /* * 对Jwt签名时,增加一个密钥 * JwtAccessTokenConverter:对Jwt来进行编码以及解码的类 / @Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey(“test-secret”); return converter; } /* * 设置token 由Jwt产生,不使用默认的透明令牌 / @Bean public JwtTokenStore jwtTokenStore() { return new JwtTokenStore(accessTokenConverter()); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints .authenticationManager(authenticationManager) .tokenStore(jwtTokenStore()) .accessTokenConverter(accessTokenConverter()); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient(“clientapp”) .secret(“123”) .scopes(“read”) //设置支持[密码模式、授权码模式、token刷新] .authorizedGrantTypes( “password”, “authorization_code”, “refresh_token”); }}资源服务器pom.xml<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId></dependency><dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> <version>2.1.3.RELEASE</version></dependency><dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> <version>1.0.10.RELEASE</version></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency>HelloController@RestController("/api")public class HelloController { @PostMapping("/api/hi") public String say(String name) { return “hi , " + name; }}OAuth2ResourceServer/* * 资源服务器 */@Configuration@EnableResourceServerpublic class OAuth2ResourceServer extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated().and() .requestMatchers().antMatchers("/api/**”); }}application.ymlserver: port: 8081security: oauth2: resource: jwt: key-value: test-secret参数说明:security.oauth2.resource.jwt.key-value:设置签名key 保持和授权服务器一致。security.oauth2.resource.jwt:项目启动过程中,检查到配置文件中有security.oauth2.resource.jwt 的配置,就会生成 jwtTokenStore 的 bean,对令牌的校验就会使用 jwtTokenStore 。验证请求令牌curl -X POST –user ‘clientapp:123’ -d ‘grant_type=password&username=user&password=123456’ http://localhost:8080/oauth/token返回JWT令牌{ “access_token”: “eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NTQ0MzExMDgsInVzZXJfbmFtZSI6InVzZXIiLCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiOGM0YWMyOTYtMDQwYS00Y2UzLTg5MTAtMWJmNjZkYTQwOTk3IiwiY2xpZW50X2lkIjoiY2xpZW50YXBwIiwic2NvcGUiOlsicmVhZCJdfQ.YAaSRN0iftmlR6Khz9UxNNEpHHn8zhZwlQrCUCPUmsU”, “token_type”: “bearer”, “refresh_token”: “eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJ1c2VyIiwic2NvcGUiOlsicmVhZCJdLCJhdGkiOiI4YzRhYzI5Ni0wNDBhLTRjZTMtODkxMC0xYmY2NmRhNDA5OTciLCJleHAiOjE1NTY5Nzk5MDgsImF1dGhvcml0aWVzIjpbIlJPTEVfVVNFUiJdLCJqdGkiOiI0ZjA5M2ZjYS04NmM0LTQxZWUtODcxZS1kZTY2ZjFhOTI0NTAiLCJjbGllbnRfaWQiOiJjbGllbnRhcHAifQ.vvAE2LcqggBv8pxuqU6RKPX65bl7Zl9dfcoIbIQBLf4”, “expires_in”: 43199, “scope”: “read”, “jti”: “8c4ac296-040a-4ce3-8910-1bf66da40997”}携带JWT令牌请求资源curl -X POST -H “authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NTQ0MzExMDgsInVzZXJfbmFtZSI6InVzZXIiLCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiOGM0YWMyOTYtMDQwYS00Y2UzLTg5MTAtMWJmNjZkYTQwOTk3IiwiY2xpZW50X2lkIjoiY2xpZW50YXBwIiwic2NvcGUiOlsicmVhZCJdfQ.YAaSRN0iftmlR6Khz9UxNNEpHHn8zhZwlQrCUCPUmsU” -d ’name=zhangsan’ http://localhost:8081/api/hi返回hi , zhangsan源码https://github.com/gf-huanchu… ...

April 5, 2019 · 2 min · jiezi

ApiBoot - ApiBoot Security Oauth 依赖使用文档

ApiBoot是一款基于SpringBoot1.x,2.x的接口服务集成基础框架, 内部提供了框架的封装集成、使用扩展、自动化完成配置,让接口开发者可以选着性完成开箱即用, 不再为搭建接口框架而犯愁,从而极大的提高开发效率。引入 ApiBoot Security Oauth在pom.xml配置文件内添加如下:<!–ApiBoot Security Oauth–><dependency> <groupId>org.minbox.framework</groupId> <artifactId>api-boot-starter-security-oauth-jwt</artifactId></dependency>ApiBoot所提供的依赖都不需要添加版本号,但是需要添加版本依赖,具体查看ApiBoot版本依赖配置参数列表ApiBoot在整合SpringSecurity、Oauth2时把配置参数进行了分离,配置列表如下所示:整合SpringSecurity配置列表配置名称介绍默认值生效方式api.boot.security.awaySpringSecurity读取用户的方式,默认为内存方式memoryallapi.boot.security.auth-prefix拦截的接口路径前缀,如:/api/users就会被默认拦截/api/memory/jdbcapi.boot.security.users配置用户列表,具体使用查看内存方式介绍无memoryapi.boot.security.ignoring-urlsSpring Security所排除的路径,默认排除Swagger、Actuator相关路径前缀/v2/api-docs/swagger-ui.html/swagger-resources/configuration/security/META-INF/resources/webjars//swagger-resources/swagger-resources/configuration/ui/actuator/memory/jdbcapi.boot.security.enable-default-store-delegate仅在Jdbc方式生效truejdbc整合Oauth2配置列表配置名称介绍默认值绑定awayapi.boot.oauth.awayOauth存储Token、读取Client信息方式memoryallapi.boot.oauth.cleint-idOauth2 Client IDApiBootmemoryapi.boot.oauth.client-secretOauth2 Client SecretApiBootSecretmemoryapi.boot.oauth.grant-types客户端授权方式Srtring[]{“password”}memoryapi.boot.oauth.scopes客户端作用域String[]{“api”}memoryapi.boot.oauth.jwt.enable是否启用JWT格式化AccessTokenfalsememory/jdbcapi.boot.oauth.jwt.sign-key使用JWT格式化AccessToken时的签名ApiBootmemory/jdbcApiBoot在整合SpringSecurity、Oauth2时配置进行了分离,也就意味着我们可以让SpringSecurity读取内存用户、Oauth2将生成的AccessToken存放到数据库,当然反过来也是可以的,相互不影响!!!内存方式(默认方式)Spring SecurityApiBoot在整合Spring Security的内存方式时,仅仅需要配置api.boot.security.users用户列表参数即可,就是这么的简单,配置用户示例如下所示:api: boot: security: # Spring Security 内存方式用户列表示例 users: - username: hengboy password: 123456 - username: apiboot password: abc321api.boot.security.users是一个List<SecurityUser>类型的集合,所以这里可以配置多个用户。Oauth2如果全部使用默认值的情况话不需要做任何配置!!!Jdbc方式前提:项目需要添加数据源依赖。Spring Security默认用户表ApiBoot在整合Spring Security的Jdbc方式时,在使用ApiBoot提供的默认结构用户表时只需要修改api.boot.security.away: jdbc即可,ApiBoot提供的用户表结构如下所示:CREATE TABLE api_boot_user_info ( UI_ID int(11) NOT NULL AUTO_INCREMENT COMMENT ‘用户编号,主键自增’, UI_USER_NAME varchar(30) DEFAULT NULL COMMENT ‘用户名’, UI_NICK_NAME varchar(50) DEFAULT NULL COMMENT ‘用户昵称’, UI_PASSWORD varchar(255) DEFAULT NULL COMMENT ‘用户密码’, UI_EMAIL varchar(30) DEFAULT NULL COMMENT ‘用户邮箱地址’, UI_AGE int(11) DEFAULT NULL COMMENT ‘用户年龄’, UI_ADDRESS varchar(200) DEFAULT NULL COMMENT ‘用户地址’, UI_IS_LOCKED char(1) DEFAULT ‘N’ COMMENT ‘是否锁定’, UI_IS_ENABLED char(1) DEFAULT ‘Y’ COMMENT ‘是否启用’, UI_STATUS char(1) DEFAULT ‘O’ COMMENT ‘O:正常,D:已删除’, UI_CREATE_TIME timestamp NULL DEFAULT current_timestamp() COMMENT ‘用户创建时间’, PRIMARY KEY (UI_ID)) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT=‘ApiBoot默认的用户信息表’;自定义用户表如果你的系统已经存在了自定义用户表结构,ApiBoot是支持的,而且很简单就可以完成整合,我们需要先修改api.boot.security.enable-default-store-delegate参数为false,如下所示:api: boot: security: # Spring Security jdbc方式用户列表示例 enable-default-store-delegate: false away: jdbc添加ApiBootStoreDelegate接口实现类,如下所示:@Componentpublic class DisableDefaultUserTableStoreDelegate implements ApiBootStoreDelegate { @Autowired private PasswordEncoder passwordEncoder; / * 用户列表示例 * 从该集合内读取用户信息 * 可以使用集合内的用户获取access_token / static List<String> users = new ArrayList() { { add(“api-boot”); add(“hengboy”); add(“yuqiyu”); } }; /* * 根据用户名查询用户信息 * * @param username 用户名 * @return * @throws UsernameNotFoundException / @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { if (!users.contains(username)) { throw new UsernameNotFoundException(“用户:” + username + “不存在”); } return new DisableDefaultUserDetails(username); } @Data @AllArgsConstructor @NoArgsConstructor class DisableDefaultUserDetails implements UserDetails { private String username; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return new ArrayList() { { add((GrantedAuthority) () -> “ROLE_USER”); } }; } /* * 示例密码使用123456 * * @return */ @Override public String getPassword() { return passwordEncoder.encode(“123456”); } @Override public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }}根据上面代码示例,我们可以通过users用户列表进行访问获取access_token。Oauth2创建Oauth所需表结构Oauth2如果使用Jdbc方式进行存储access_token、client_details时,需要在数据库内初始化Oauth2所需相关表结构,oauth-mysql.sql添加客户端数据初始化Oauth2表结构后,需要向oauth_client_details表内添加一个客户端信息,下面是对应ApiBoot Security Oauth配置信息的数据初始化,如下所示:INSERT INTO oauth_client_details VALUES (‘ApiBoot’,‘api’,’$2a$10$M5t8t1fHatAj949RCHHB/.j1mrNAbxIz.mOYJQbMCcSPwnBMJLmMK’,‘api’,‘password’,NULL,NULL,7200,7200,NULL,NULL);AppSecret加密方式统一使用BCryptPasswordEncoder,数据初始化时需要注意。在上面memory/jdbc两种方式已经配置完成,接下来我们就可以获取access_token。获取AccessToken通过CURL获取➜ ~ curl ApiBoot:ApiBootSecret@localhost:8080/oauth/token -d “grant_type=password&username=api-boot&password=123456”{“access_token”:“eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NTMxMDk1MjMsInVzZXJfbmFtZSI6ImFwaS1ib290IiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9VU0VSIl0sImp0aSI6IjBmZTUyY2RlLTBhZjctNDI1YS04Njc2LTFkYTUyZTA0YzUxYiIsImNsaWVudF9pZCI6IkFwaUJvb3QiLCJzY29wZSI6WyJhcGkiXX0.ImqGZssbDEOmpf2lQZjLQsch4ukE0C4SCYJsutfwfx0”,“token_type”:“bearer”,“expires_in”:42821,“scope”:“api”,“jti”:“0fe52cde-0af7-425a-8676-1da52e04c51b”}启用JWTApiBoot Security Oauth在使用JWT格式化access_token时非常简单的,配置如下所示:api: boot: oauth: jwt: # 开启Jwt转换AccessToken enable: true # 转换Jwt时所需加密key,默认为ApiBoot sign-key: 恒宇少年 - 于起宇默认不启用JWT,sign-key签名建议进行更换。本章源码地址:https://github.com/hengboy/api-boot/tree/master/api-boot-samples/api-boot-sample-security-oauth-jwt ...

April 4, 2019 · 2 min · jiezi

Spring 官方文档完整翻译

以下所有文档均包含多个版本,并支持多语言(英文及中文)。Spring Boot 中文文档Spring Framework 中文文档Spring Cloud 中文文档Spring Security 中文文档Spring Session 中文文档Spring AMQP 中文文档Spring DataSpring Data JPASpring Data JDBCSpring Data RedisContributing如果你希望参与文档的校对及翻译工作,请在 这里 提 PR。

April 3, 2019 · 1 min · jiezi

Spring Security项目Spring MVC开发RESTful API(二)

查询请求常用注解@RestController 标明此Controller提供RestAPI@RequestMapping 映射http请求url到java方法@RequestParam 映射请求参数到java方法到参数@PageableDefault 指定分页参数默认值编写一个简单的UserController类@RestController@RequestMapping(value = “/user”)public class UserController { @RequestMapping(method = RequestMethod.GET) public List<User> query(@RequestParam(name = “username”,required = true) String username, @PageableDefault(page = 1,size = 20,sort = “username”,direction = Sort.Direction.DESC)Pageable pageable){ System.out.println(pageable.getSort()); List<User>users=new ArrayList<>(); users.add(new User(“aaa”,“111”)); users.add(new User(“bbb”,“222”)); users.add(new User(“ddd”,“333”)); return users; }}@PageableDefault SpingData分页参数 page当前页数默认0开始 sizi每页个数默认10 sort 排序Srping boot 测试用例在demo的pom.xml里面引入spirngboot的测试 <!–spring测试框架–> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency>测试/user接口@RunWith(SpringRunner.class) //运行器@SpringBootTestpublic class UserControllerTest { @Autowired private WebApplicationContext wac; private MockMvc mockMvc; @Before public void stup(){ mockMvc= MockMvcBuilders.webAppContextSetup(wac).build(); } //测试用例 @Test public void whenQuerSuccess() throws Exception { String result=mockMvc.perform(MockMvcRequestBuilders.get("/user") //传过去的参数 .param(“username”,“admin”) .contentType(MediaType.APPLICATION_JSON_UTF8)) //判断请求的状态吗是否成功,200 .andExpect(MockMvcResultMatchers.status().isOk()) //判断返回的集合的长度是否是3 .andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(3)) //打印信息 .andDo(MockMvcResultHandlers.print()) .andReturn().getResponse().getContentAsString(); //打印返回结果 System.out.println(result); }jsonPath文档语法查询地址用户详情请求常用注解@PathVariable 映射url片段到java方法参数@JsonView 控制json输出内容实体对象@NoArgsConstructor@AllArgsConstructorpublic class User { public interface UserSimpleView{}; public interface UserDetailView extends UserSimpleView{}; private String username; private String password; @JsonView(UserSimpleView.class) public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } @JsonView(UserDetailView.class) public String getPassword() { return password; } public void setPassword(String password) { this.password = password; }}Controller类@RestController@RequestMapping(value = “/user”)public class UserController { @RequestMapping(value = “/{id:\d+}",method = RequestMethod.GET) // 正则表达式 :\d+ 表示只能输入数字 //用户名密码都显示 @JsonView(User.UserDetailView.class) public User userInfo(@PathVariable String id){ User user=new User(); user.setUsername(“tom”); return user; }}测试用例@RunWith(SpringRunner.class) //运行器@SpringBootTestpublic class UserControllerTest { @Autowired private WebApplicationContext wac; private MockMvc mockMvc; @Before public void stup(){ mockMvc= MockMvcBuilders.webAppContextSetup(wac).build(); } //用户详情用例 @Test public void whenUserInfoSuccess() throws Exception { String result=mockMvc.perform(MockMvcRequestBuilders.get("/user/1”) .contentType(MediaType.APPLICATION_JSON_UTF8)) //判断请求的状态吗是否成功,200 .andExpect(MockMvcResultMatchers.status().isOk()) //判断返回到username是不是tom .andExpect(MockMvcResultMatchers.jsonPath("$.username").value(“tom”)) //打印信息 .andDo(MockMvcResultHandlers.print()) .andReturn().getResponse().getContentAsString(); //打印返回结果 System.out.println(result); }}用户处理创建请求常用注解@RequestBody 映射请求体到java方法到参数@Valid注解和BindingResult验证请求参数合法性并处理校验结果实体对象@NoArgsConstructor@AllArgsConstructorpublic class User { public interface UserSimpleView{}; public interface UserDetailView extends UserSimpleView{}; private String id; private String username; //不允许password为null @NotBlank private String password; private Date birthday; @JsonView(UserSimpleView.class) public String getId() { return id; } public void setId(String id) { this.id = id; } @JsonView(UserSimpleView.class) public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } @JsonView(UserDetailView.class) public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @JsonView(UserSimpleView.class) public Date getBirthday() { return birthday; } public void setBirthday(Date birthday) { this.birthday = birthday; }}Controller类 @RequestMapping(method = RequestMethod.POST) @JsonView(User.UserSimpleView.class) //@Valid启用校验password不允许为空 public User createUser(@Valid @RequestBody User user, BindingResult errors){ //如果校验有错误是true并打印错误信息 if(errors.hasErrors()){ errors.getAllErrors().stream().forEach(error -> System.out.println(error.getDefaultMessage())); } System.out.println(user.getUsername()); System.out.println(user.getPassword()); System.out.println(user.getBirthday()); user.setId(“1”); return user; }测试用例@RunWith(SpringRunner.class) //运行器@SpringBootTestpublic class UserControllerTest { @Autowired private WebApplicationContext wac; private MockMvc mockMvc; @Before public void stup(){ mockMvc= MockMvcBuilders.webAppContextSetup(wac).build(); } //用户创建用例 @Test public void whenCreateSuccess() throws Exception { Date date=new Date(); String content="{"username":"tom","password":null,"birthday":"+date.getTime()+"}"; String result=mockMvc.perform(MockMvcRequestBuilders.post("/user") .content(content) .contentType(MediaType.APPLICATION_JSON_UTF8)) //判断请求的状态吗是否成功,200 .andExpect(MockMvcResultMatchers.status().isOk()) //判断返回到username是不是tom .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(“1”)) .andReturn().getResponse().getContentAsString(); //打印返回结果 System.out.println(result); }}修改和删除请求验证注解| 注解 | 解释 | | ——– | ——– | | @NotNull | 值不能为空 | | @Null | 值必须为空 | | @Pattern(regex=) | 字符串必须匹配正则表达式 | | @Size(min=,max=) | 集合的元素数量必须在min和max之间 | | @Email | 字符串必须是Email地址 | | @Length(min=,max=) | 检查字符串长度 | | @NotBlank | 字符串必须有字符 | | @NotEmpty | 字符串不为null,集合有元素 | | @Range(min=,max=) | 数字必须大于等于min,小于等于max | | @SafeHtml | 字符串是安全的html | | @URL | 字符串是合法的URL | | @AssertFalse | 值必须是false | | @AssertTrue | 值必须是true | | @DecimalMax(value=,inclusive) | 值必须小于等于(inclusive=true)/小于(inclusive=false) value指定的值 | | @DecimalMin(value=,inclusive) | 值必须大于等于(inclusive=true)/大于(inclusive=false) value指定的值 | | @Digits(integer=,fraction=) | integer指定整数部分最大长度,fraction小数部分最大长度 | | @Future | 被注释的元素必须是一个将来的日期 | | @Past | 被注释的元素必须是一个过去的日期 | | @Max(value=) | 值必须小于等于value值 | | @Min(value=) | 值必须大于等于value值 |自定义注解修改请求实体对象@NoArgsConstructor@AllArgsConstructorpublic class User { public interface UserSimpleView{}; public interface UserDetailView extends UserSimpleView{}; private String id; //自定义注解 @MyConstraint(message = “账号必须是tom”) private String username; //不允许password为null @NotBlank(message = “密码不能为空”) private String password; //加验证生日必须是过去的时间 @Past(message = “生日必须是过去的时间”) private Date birthday; @JsonView(UserSimpleView.class) public String getId() { return id; } public void setId(String id) { this.id = id; } @JsonView(UserSimpleView.class) public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } @JsonView(UserDetailView.class) public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @JsonView(UserSimpleView.class) public Date getBirthday() { return birthday; } public void setBirthday(Date birthday) { this.birthday = birthday; }}Controller类 @RequestMapping(value = “/{id:\d+}",method = RequestMethod.PUT) @JsonView(User.UserSimpleView.class) //@Valid启用校验password不允许为空 public User updateUser(@Valid @RequestBody User user, BindingResult errors){ //如果校验有错误是true并打印错误信息 if(errors.hasErrors()){ errors.getAllErrors().stream().forEach(error -> System.out.println(error.getDefaultMessage())); } System.out.println(user.getUsername()); System.out.println(user.getPassword()); System.out.println(user.getBirthday()); user.setId(“1”); return user; }测试用例@RunWith(SpringRunner.class) //运行器@SpringBootTestpublic class UserControllerTest { @Autowired private WebApplicationContext wac; private MockMvc mockMvc; @Before public void stup(){ mockMvc= MockMvcBuilders.webAppContextSetup(wac).build(); } //用户修改用例 @Test public void whenUpdateSuccess() throws Exception { //当前时间加一年 Date date = new Date(LocalDateTime.now().plusYears(1).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); String content = “{"id":"1","username":"44","password":null,"birthday":” + date.getTime() + “}”; String result = mockMvc.perform(MockMvcRequestBuilders.put("/user/1”) .content(content) .contentType(MediaType.APPLICATION_JSON_UTF8)) //判断请求的状态吗是否成功,200 .andExpect(MockMvcResultMatchers.status().isOk()) //判断返回到username是不是tom .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(“1”)) .andReturn().getResponse().getContentAsString(); //打印返回结果 System.out.println(result); }自定义注解MyConstraint类import javax.validation.Constraint;import javax.validation.Payload;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;//作用在字段跟方法上面@Target({ElementType.FIELD,ElementType.METHOD})//运行时注解@Retention(RetentionPolicy.RUNTIME)//需要校验注解的类@Constraint(validatedBy = MyConstraintValidator.class)public @interface MyConstraint { String message() default “{org.hibernate.validator.constraints.NotBlank.message}”; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {};}MyConstraintValidator类import javax.validation.ConstraintValidator;import javax.validation.ConstraintValidatorContext;//范型1.验证的注解 2.验证的数据类型public class MyConstraintValidator implements ConstraintValidator<MyConstraint,Object> { @Override public void initialize(MyConstraint myConstraint) { //校验器初始化的规则 } @Override public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) { //校验username如果是tom验证通过 if (value.equals(“tom”)){ return true; }else{ return false; } }}删除请求Controller类 @RequestMapping(value = “/{id:\d+}",method = RequestMethod.DELETE) //@Valid启用校验password不允许为空 public void deleteUser(@PathVariable String id){ System.out.println(id); }测试用例@RunWith(SpringRunner.class) //运行器@SpringBootTestpublic class UserControllerTest { @Autowired private WebApplicationContext wac; private MockMvc mockMvc; @Before public void stup(){ mockMvc= MockMvcBuilders.webAppContextSetup(wac).build(); } //用户删除用例 @Test public void whenDeleteSuccess() throws Exception { mockMvc.perform(MockMvcRequestBuilders.delete("/user/1”) .contentType(MediaType.APPLICATION_JSON_UTF8)) //判断请求的状态吗是否成功,200 .andExpect(MockMvcResultMatchers.status().isOk()); }服务异常处理把BindingResult errors去掉 @RequestMapping(method = RequestMethod.POST) @JsonView(User.UserSimpleView.class) //@Valid启用校验password不允许为空 public User createUser(@Valid @RequestBody User user){ //如果校验有错误是true并打印错误信息// if(errors.hasErrors()){// errors.getAllErrors().stream().forEach(error -> System.out.println(error.getDefaultMessage()));// } System.out.println(user.getUsername()); System.out.println(user.getPassword()); System.out.println(user.getBirthday()); user.setId(“1”); return user; }查看返回的异常信息处理状态码错误创建文件结构如下404错误将跳转对应页面RESTful API的拦截过滤器(Filter)创建filter文件@Componentpublic class TimeFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { System.out.println(“TimeFilter init”); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println(“TimeFilter doFilter”); long start=new Date().getTime(); filterChain.doFilter(servletRequest,servletResponse); System.out.println(“耗时”+(new Date().getTime()-start)); } @Override public void destroy() { System.out.println(“TimeFilter destroy”); }}自定义filter需要吧filter文件@Component标签去除@Configurationpublic class WebConfig { @Bean public FilterRegistrationBean timeFilterRegistration(){ FilterRegistrationBean registration=new FilterRegistrationBean(); TimeFilter timeFilter=new TimeFilter(); registration.setFilter(timeFilter); //filter作用的地址 List<String>urls=new ArrayList<>(); urls.add("/user"); registration.setUrlPatterns(urls); return registration; }}拦截器(Interceptor)创建Interceptor文件@Componentpublic class TimeInterceptor implements HandlerInterceptor { //控制器方法调用之前 @Override public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception { System.out.println(“preHandle”); System.out.println(“进入方法”+((HandlerMethod)o).getMethod().getName()); httpServletRequest.setAttribute(“startTime”,new Date().getTime()); //是否调用后面的方法调用是true return true; } //控制器方法被调用 @Override public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { System.out.println(“postHandle”); Long start= (Long) httpServletRequest.getAttribute(“startTime”); System.out.println(“time interceptor耗时”+(new Date().getTime()-start)); } //控制器方法完成之后 @Override public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { System.out.println(“afterCompletion”); System.out.println(“exception is”+e); }}把过滤器添加到webconfig文件@Configurationpublic class WebConfig extends WebMvcConfigurerAdapter { @Autowired private TimeInterceptor timeInterceptor; //过滤器 @Bean public FilterRegistrationBean timeFilterRegistration(){ FilterRegistrationBean registration=new FilterRegistrationBean(); TimeFilter timeFilter=new TimeFilter(); registration.setFilter(timeFilter); //filter作用的地址 List<String>urls=new ArrayList<>(); urls.add("/user/"); registration.setUrlPatterns(urls); return registration; } //拦截器 @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(timeInterceptor); }}切片(Aspect)@Aspect@Componentpublic class TimeAspect { //@Befor方法调用之前 //@After()方法调用 //@AfterThrowing方法调用之后 //包围,覆盖前面三种 @Around(“execution( com.guosh.web.controller.UserController.(..))”)//表达式表示usercontroller里所有方法其他表达式可以查询切片表达式 public Object handleControllerMethod(ProceedingJoinPoint pjp) throws Throwable { System.out.println(“time aspect start”); //可以获取到传入参数 Object[]args=pjp.getArgs(); for (Object arg: args) { System.out.println(“arg is”+arg); } long start=new Date().getTime(); //相当于filter里doFilter方法 Object object=pjp.proceed(); System.out.println(“time aspect耗时”+(new Date().getTime()-start)); System.out.println(“time aspect end”); return object; }}总结过滤器Filter :可以拿到原始的http请求与响应信息拦截器Interceptor :可以拿到原始的http请求与响应信息还可以拿到处理请求方法的信息切片Aspect :可以拿到方法调用传过来的值使用rest方式处理文件服务返回的上传文件后路径对象在application.yml里添加上传地址#上传文件路径uploadfiledir: filePath: /Users/shaohua/webapp/guoshsecurity@Data@NoArgsConstructor@AllArgsConstructorpublic class FileInfo { private String path;}@RestController@RequestMapping("/file")public class FileController { @Value("${uploadfiledir.filePath}") private String fileDataStorePath;//文件上传地址 @RequestMapping(method = RequestMethod.POST) public FileInfo upload(@RequestParam(“file”) MultipartFile file) throws IOException { //文件名 System.out.println(file.getOriginalFilename()); //文件大小 System.out.println(file.getSize()); //获取文件后缀名 String ext=StringUtils.getFilenameExtension(file.getOriginalFilename()); File fileDir = new File(fileDataStorePath); //判断是否创建目录 if (!fileDir.exists()) { if (!fileDir.mkdirs() || !fileDir.exists()) { // 创建目录失败 throw new RuntimeException(“无法创建目录!”); } } File localFile=new File(fileDataStorePath, UUID.randomUUID().toString().replace("-", “”)+"."+ext); file.transferTo(localFile); //返回上传的路径地址 return new FileInfo(localFile.getAbsolutePath()); } //下载文件 @RequestMapping(value ="/{id}" ,method = RequestMethod.GET) public void download(@PathVariable String id, HttpServletResponse response){ //模拟下载直接填好了下载文件名称 try(InputStream inputStream = new FileInputStream(new File(fileDataStorePath,“13a2c075b7f44025bbb3c590f7f372eb.txt”)); OutputStream outputStream=response.getOutputStream();){ response.setContentType(“application/x-download”); response.addHeader(“Content-Disposition”,“attachment;filename="+“13a2c075b7f44025bbb3c590f7f372eb.txt"”); IOUtils.copy(inputStream,outputStream); } catch (Exception e) { e.printStackTrace(); } }}使用Swagger工具在demo模块引入 <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency>添加swagger的配置类@Configuration@EnableSwagger2public class Swagger2Config { @Value("${sys.swagger.enable-swgger}”) private Boolean enableSwgger; @Bean public Docket createRestApi() { return new Docket(DocumentationType.SWAGGER_2) .enable(enableSwgger) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage(“com.guosh.web”)) //swgger插件作用范围 //.paths(PathSelectors.regex("/api/.")) .paths(PathSelectors.any()) //过滤接口 .build(); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title(“SpringSecurityDemo”) //标题 .description(“API描述”) //描述 .contact(new Contact(“guoshaohua”, “http://www.guoshaohua.cn”, “”))//作者 .version(“1.0”) .build(); }}常用注解通过@Api用于controller类上对类的功能进行描述通过@ApiOperation注解用在controller方法上对类的方法进行描述通过@ApiImplicitParams、@ApiImplicitParam注解来给参数增加说明通过@ApiIgnore来忽略那些不想让生成RESTful API文档的接口通过@ApiModel 用在返回对象类上描述返回对象的意义通过@ApiModelProperty 用在实体对象的字段上 用于描述字段含义 ...

March 30, 2019 · 6 min · jiezi

基于Spring Security和 JWT的权限系统设计

写在前面关于 Spring SecurityWeb系统的认证和权限模块也算是一个系统的基础设施了,几乎任何的互联网服务都会涉及到这方面的要求。在Java EE领域,成熟的安全框架解决方案一般有 Apache Shiro、Spring Security等两种技术选型。Apache Shiro简单易用也算是一大优势,但其功能还是远不如 Spring Security强大。Spring Security可以为 Spring 应用提供声明式的安全访问控制,起通过提供一系列可以在 Spring应用上下文中可配置的Bean,并利用 Spring IoC和 AOP等功能特性来为应用系统提供声明式的安全访问控制功能,减少了诸多重复工作。关于JWTJSON Web Token (JWT),是在网络应用间传递信息的一种基于 JSON的开放标准((RFC 7519),用于作为JSON对象在不同系统之间进行安全地信息传输。主要使用场景一般是用来在 身份提供者和服务提供者间传递被认证的用户身份信息。关于JWT的科普,可以看看阮一峰老师的《JSON Web Token 入门教程》。本文则结合 Spring Security和 JWT两大利器来打造一个简易的权限系统。本文实验环境如下:Spring Boot版本:2.0.6.RELEASEIDE:IntelliJ IDEA 2018.2.4另外本文实验代码置于文尾,需要自取。设计用户和角色本文实验为了简化考虑,准备做如下设计:设计一个最简角色表role,包括角色ID和角色名称设计一个最简用户表user,包括用户ID,用户名,密码再设计一个用户和角色一对多的关联表user_roles一个用户可以拥有多个角色创建 Spring Security和 JWT加持的 Web工程pom.xml 中引入 Spring Security和 JWT所必需的依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency><dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version></dependency>项目配置文件中加入数据库和 JPA等需要的配置server.port=9991spring.datasource.driver-class-name=com.mysql.jdbc.Driverspring.datasource.url=jdbc:mysql://121.196.XXX.XXX:3306/spring_security_jwt?useUnicode=true&characterEncoding=utf-8spring.datasource.username=rootspring.datasource.password=XXXXXXlogging.level.org.springframework.security=infospring.jpa.hibernate.ddl-auto=updatespring.jpa.show-sql=truespring.jackson.serialization.indent_output=true创建用户、角色实体用户实体 User:/** * @ www.codesheep.cn * 20190312 /@Entitypublic class User implements UserDetails { @Id @GeneratedValue private Long id; private String username; private String password; @ManyToMany(cascade = {CascadeType.REFRESH},fetch = FetchType.EAGER) private List<Role> roles; … // 下面为实现UserDetails而需要的重写方法! @Override public Collection<? extends GrantedAuthority> getAuthorities() { List<GrantedAuthority> authorities = new ArrayList<>(); for (Role role : roles) { authorities.add( new SimpleGrantedAuthority( role.getName() ) ); } return authorities; } …}此处所创建的 User类继承了 Spring Security的 UserDetails接口,从而成为了一个符合 Security安全的用户,即通过继承 UserDetails,即可实现 Security中相关的安全功能。角色实体 Role:/* * @ www.codesheep.cn * 20190312 /@Entitypublic class Role { @Id @GeneratedValue private Long id; private String name; … // 省略 getter和 setter}创建JWT工具类主要用于对 JWT Token进行各项操作,比如生成Token、验证Token、刷新Token等/* * @ www.codesheep.cn * 20190312 /@Componentpublic class JwtTokenUtil implements Serializable { private static final long serialVersionUID = -5625635588908941275L; private static final String CLAIM_KEY_USERNAME = “sub”; private static final String CLAIM_KEY_CREATED = “created”; public String generateToken(UserDetails userDetails) { … } String generateToken(Map<String, Object> claims) { … } public String refreshToken(String token) { … } public Boolean validateToken(String token, UserDetails userDetails) { … } … // 省略部分工具函数}创建Token过滤器,用于每次外部对接口请求时的Token处理/* * @ www.codesheep.cn * 20190312 /@Componentpublic class JwtTokenFilter extends OncePerRequestFilter { @Autowired private UserDetailsService userDetailsService; @Autowired private JwtTokenUtil jwtTokenUtil; @Override protected void doFilterInternal ( HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String authHeader = request.getHeader( Const.HEADER_STRING ); if (authHeader != null && authHeader.startsWith( Const.TOKEN_PREFIX )) { final String authToken = authHeader.substring( Const.TOKEN_PREFIX.length() ); String username = jwtTokenUtil.getUsernameFromToken(authToken); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); if (jwtTokenUtil.validateToken(authToken, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails( request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } } chain.doFilter(request, response); }}Service业务编写主要包括用户登录和注册两个主要的业务public interface AuthService { User register( User userToAdd ); String login( String username, String password );}/* * @ www.codesheep.cn * 20190312 /@Servicepublic class AuthServiceImpl implements AuthService { @Autowired private AuthenticationManager authenticationManager; @Autowired private UserDetailsService userDetailsService; @Autowired private JwtTokenUtil jwtTokenUtil; @Autowired private UserRepository userRepository; // 登录 @Override public String login( String username, String password ) { UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken( username, password ); final Authentication authentication = authenticationManager.authenticate(upToken); SecurityContextHolder.getContext().setAuthentication(authentication); final UserDetails userDetails = userDetailsService.loadUserByUsername( username ); final String token = jwtTokenUtil.generateToken(userDetails); return token; } // 注册 @Override public User register( User userToAdd ) { final String username = userToAdd.getUsername(); if( userRepository.findByUsername(username)!=null ) { return null; } BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); final String rawPassword = userToAdd.getPassword(); userToAdd.setPassword( encoder.encode(rawPassword) ); return userRepository.save(userToAdd); }}Spring Security配置类编写(非常重要)这是一个高度综合的配置类,主要是通过重写 WebSecurityConfigurerAdapter 的部分 configure配置,来实现用户自定义的部分。/* * @ www.codesheep.cn * 20190312 /@Configuration@EnableWebSecurity@EnableGlobalMethodSecurity(prePostEnabled=true)public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserService userService; @Bean public JwtTokenFilter authenticationTokenFilterBean() throws Exception { return new JwtTokenFilter(); } @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure( AuthenticationManagerBuilder auth ) throws Exception { auth.userDetailsService( userService ).passwordEncoder( new BCryptPasswordEncoder() ); } @Override protected void configure( HttpSecurity httpSecurity ) throws Exception { httpSecurity.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .authorizeRequests() .antMatchers(HttpMethod.OPTIONS, “/”).permitAll() // OPTIONS请求全部放行 .antMatchers(HttpMethod.POST, “/authentication/”).permitAll() //登录和注册的接口放行,其他接口全部接受验证 .antMatchers(HttpMethod.POST).authenticated() .antMatchers(HttpMethod.PUT).authenticated() .antMatchers(HttpMethod.DELETE).authenticated() .antMatchers(HttpMethod.GET).authenticated(); // 使用前文自定义的 Token过滤器 httpSecurity .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class); httpSecurity.headers().cacheControl(); }}编写测试 Controller登录和注册的 Controller:/* * @ www.codesheep.cn * 20190312 /@RestControllerpublic class JwtAuthController { @Autowired private AuthService authService; // 登录 @RequestMapping(value = “/authentication/login”, method = RequestMethod.POST) public String createToken( String username,String password ) throws AuthenticationException { return authService.login( username, password ); // 登录成功会返回JWT Token给用户 } // 注册 @RequestMapping(value = “/authentication/register”, method = RequestMethod.POST) public User register( @RequestBody User addedUser ) throws AuthenticationException { return authService.register(addedUser); }}再编写一个测试权限的 Controller:/* * @ www.codesheep.cn * 20190312 */@RestControllerpublic class TestController { // 测试普通权限 @PreAuthorize(“hasAuthority(‘ROLE_NORMAL’)”) @RequestMapping( value="/normal/test", method = RequestMethod.GET ) public String test1() { return “ROLE_NORMAL /normal/test接口调用成功!”; } // 测试管理员权限 @PreAuthorize(“hasAuthority(‘ROLE_ADMIN’)”) @RequestMapping( value = “/admin/test”, method = RequestMethod.GET ) public String test2() { return “ROLE_ADMIN /admin/test接口调用成功!”; }}这里给出两个测试接口用于测试权限相关问题,其中接口 /normal/test需要用户具备普通角色(ROLE_NORMAL)即可访问,而接口/admin/test则需要用户具备管理员角色(ROLE_ADMIN)才可以访问。接下来启动工程,实验测试看看效果—实验验证在文章开头我们即在用户表 user中插入了一条用户名为 codesheep的记录,并在用户-角色表 user_roles中给用户 codesheep分配了普通角色(ROLE_NORMAL)和管理员角色(ROLE_ADMIN)接下来进行用户登录,并获得后台向用户颁发的JWT Token接下来访问权限测试接口不带 Token直接访问需要普通角色(ROLE_NORMAL)的接口 /normal/test会直接提示访问不通:而带 Token访问需要普通角色(ROLE_NORMAL)的接口 /normal/test才会调用成功:同理由于目前用户具备管理员角色,因此访问需要管理员角色(ROLE_ADMIN)的接口 /admin/test也能成功:接下里我们从用户-角色表里将用户codesheep的管理员权限删除掉,再访问接口 /admin/test,会发现由于没有权限,访问被拒绝了:经过一系列的实验过程,也达到了我们的预期!写在最后本文涉及的东西还是蛮多的,最后我们也将本文的实验源码放在 Github上,需要的可以自取:源码下载地址由于能力有限,若有错误或者不当之处,还请大家批评指正,一起学习交流!My Personal Blog:CodeSheep 程序羊 ...

March 14, 2019 · 3 min · jiezi

使用SpringSecurity处理CSRF攻击

CSRF漏洞现状CSRF(Cross-site request forgery)跨站请求伪造,也被称为One Click Attack或者Session Riding,通常缩写为CSRF或XSRF,是一种对网站的恶意利用。尽管听起来像跨站脚本(XSS),但它与XSS非常不同,XSS利用站点内的信任用户,而CSRF则通过伪装成受信任用户的请求来利用受信任的网站。与XSS攻击相比,CSRF攻击往往不大流行(因此对其进行防范的资源也相当稀少)和难以防范,所以被认为比XSS更具危险性。 CSRF是一种依赖web浏览器的、被混淆过的代理人攻击(deputy attack)。POM依赖<!– 模板引擎 freemarker –><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId></dependency><!– Security (只使用CSRF部分) –><dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId></dependency>配置过滤器@SpringBootApplicationpublic class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } /** * 配置CSRF过滤器 * * @return {@link org.springframework.boot.web.servlet.FilterRegistrationBean} / @Bean public FilterRegistrationBean<CsrfFilter> csrfFilter() { FilterRegistrationBean<CsrfFilter> registration = new FilterRegistrationBean<>(); registration.setFilter(new CsrfFilter(new HttpSessionCsrfTokenRepository())); registration.addUrlPatterns("/"); registration.setName(“csrfFilter”); return registration; }}在form请求中添加CSRF的隐藏字段<input name="${(_csrf.parameterName)!}" value="${(_csrf.token)!}" type=“hidden” />在AJAX请求中添加header头xhr.setRequestHeader("${_csrf.headerName}", “${_csrf.token}”);jQuery的Ajax全局配置jQuery.ajaxSetup({ “beforeSend”: function (request) { request.setRequestHeader("${_csrf.headerName}", “${_csrf.token}”); }});

March 6, 2019 · 1 min · jiezi

Spring Security 单点登录简单示例

本文为[原创]文章,转载请标明出处。本文链接:https://weyunx.com/2019/02/12…本文出自微云的技术博客Overview最近在弄单点登录,踩了不少坑,所以记录一下,做了个简单的例子。目标:认证服务器认证后获取 token,客户端访问资源时带上 token 进行安全验证。可以直接看源码。关键依赖<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.2.RELEASE</version> <relativePath/></parent><dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> <version>2.1.2.RELEASE</version> </dependency></dependencies>认证服务器认证服务器的关键代码有如下几个文件:AuthServerApplication:@SpringBootApplication@EnableResourceServerpublic class AuthServerApplication { public static void main(String[] args) { SpringApplication.run(AuthServerApplication.class, args); }}AuthorizationServerConfiguration 认证配置:@Configuration@EnableAuthorizationServerclass AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { @Autowired AuthenticationManager authenticationManager; @Autowired TokenStore tokenStore; @Autowired BCryptPasswordEncoder encoder; @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { //配置客户端 clients .inMemory() .withClient(“client”) .secret(encoder.encode(“123456”)).resourceIds(“hi”) .authorizedGrantTypes(“password”,“refresh_token”) .scopes(“read”); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints .tokenStore(tokenStore) .authenticationManager(authenticationManager); } @Override public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception { //允许表单认证 oauthServer .allowFormAuthenticationForClients() .checkTokenAccess(“permitAll()”) .tokenKeyAccess(“permitAll()”); }}代码中配置了一个 client,id 是 client,密码 123456。 authorizedGrantTypes 有 password 和refresh_token 两种方式。SecurityConfiguration 安全配置:@Configuration@EnableWebSecuritypublic class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Bean public TokenStore tokenStore() { return new InMemoryTokenStore(); } @Bean public BCryptPasswordEncoder encoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .passwordEncoder(encoder()) .withUser(“user_1”).password(encoder().encode(“123456”)).roles(“USER”) .and() .withUser(“user_2”).password(encoder().encode(“123456”)).roles(“ADMIN”); } @Override protected void configure(HttpSecurity http) throws Exception { // @formatter:off http.csrf().disable() .requestMatchers() .antMatchers("/oauth/authorize") .and() .authorizeRequests() .anyRequest().authenticated() .and() .formLogin().permitAll(); // @formatter:on } @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }}上面在内存中创建了两个用户,角色分别是 USER 和 ADMIN。后续可考虑在数据库或者 Redis 中存储相关信息。AuthUser 配置获取用户信息的 Controller:@RestControllerpublic class AuthUser { @GetMapping("/oauth/user") public Principal user(Principal principal) { return principal; }}application.yml 配置,主要就是配置个端口号:—spring: profiles: active: dev application: name: auth-serverserver: port: 8101客户端配置客户端的配置比较简单,主要代码结构如下:application.yml 配置:—spring: profiles: active: dev application: name: clientserver: port: 8102security: oauth2: client: client-id: client client-secret: 123456 access-token-uri: http://localhost:8101/oauth/token user-authorization-uri: http://localhost:8101/oauth/authorize scope: read use-current-uri: false resource: user-info-uri: http://localhost:8101/oauth/user这里主要是配置了认证服务器的相关地址以及客户端的 id 和 密码。user-info-uri 配置的就是服务器端获取用户信息的接口。HelloController 访问的资源,配置了 ADMIN 的角色才可以访问:@RestControllerpublic class HelloController { @RequestMapping("/hi") @PreAuthorize(“hasRole(‘ADMIN’)”) public ResponseEntity<String> hi() { return ResponseEntity.ok().body(“auth success!”); }}WebSecurityConfiguration 相关安全配置:@Configuration@EnableOAuth2Sso@EnableGlobalMethodSecurity(prePostEnabled = true) class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http .csrf().disable() // 基于token,所以不需要session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .anyRequest().authenticated(); }}其中 @EnableGlobalMethodSecurity(prePostEnabled = true) 开启后,Spring Security 的 @PreAuthorize,@PostAuthorize 注解才可以使用。@EnableOAuth2Sso 配置了单点登录。ClientApplication:@SpringBootApplication@EnableResourceServerpublic class ClientApplication { public static void main(String[] args) { SpringApplication.run(ClientApplication.class, args); }}验证启动项目后,我们使用 postman 来进行验证。首先是获取 token:选择 POST 提交,地址为验证服务器的地址,参数中输入 username,password,grant_type 和 scope ,其中 grant_type 需要输入 password。然后在下面等 Authorization 标签页中,选择 Basic Auth,然后输入 client 的 id 和 password。{ “access_token”: “02f501a9-c482-46d4-a455-bf79a0e0e728”, “token_type”: “bearer”, “refresh_token”: “0e62dddc-4f51-4cb5-81c3-5383fddbb81b”, “expires_in”: 41741, “scope”: “read”}此时就可以获得 access_token 为: 02f501a9-c482-46d4-a455-bf79a0e0e728。需要注意的是这里是用 user_2 获取的 token,即角色是 ADMIN。然后我们再进行获取资源的验证:使用 GET 方法,参数中输入 access_token,值输入 02f501a9-c482-46d4-a455-bf79a0e0e728 。点击提交后即可获取到结果。如果我们不加上 token ,则会提示无权限。同样如果我们换上 user_1 获取的 token,因 user_1 的角色是 USER,此资源需要 ADMIN 权限,则此处还是会获取失败。简单的例子就到这,后续有时间再加上其它功能吧,谢谢~未完待续… ...

February 27, 2019 · 2 min · jiezi

8.1.4 Authentication in a Web Application

8.1.4 Authentication in a Web ApplicationNow let’s explore the situation where you are using Spring Security in a web application (without web.xml security enabled). How is a user authenticated and the security context established?Consider a typical web application’s authentication process:You visit the home page, and click on a link.A request goes to the server, and the server decides that you’ve asked for a protected resource.As you’re not presently authenticated, the server sends back a response indicating that you must authenticate. The response will either be an HTTP response code, or a redirect to a particular web page.Depending on the authentication mechanism, your browser will either redirect to the specific web page so that you can fill out the form, or the browser will somehow retrieve your identity (via a BASIC authentication dialogue box, a cookie, a X.509 certificate etc.).The browser will send back a response to the server. This will either be an HTTP POST containing the contents of the form that you filled out, or an HTTP header containing your authentication details.Next the server will decide whether or not the presented credentials are valid. If they’re valid, the next step will happen. If they’re invalid, usually your browser will be asked to try again (so you return to step two above).The original request that you made to cause the authentication process will be retried. Hopefully you’ve authenticated with sufficient granted authorities to access the protected resource. If you have sufficient access, the request will be successful. Otherwise, you’ll receive back an HTTP error code 403, which means “forbidden”.Spring Security has distinct classes responsible for most of the steps described above. The main participants (in the order that they are used) are the ExceptionTranslationFilter, an AuthenticationEntryPoint and an “authentication mechanism”, which is responsible for calling the AuthenticationManager which we saw in the previous section.ExceptionTranslationFilterExceptionTranslationFilter is a Spring Security filter that has responsibility for detecting any Spring Security exceptions that are thrown. Such exceptions will generally be thrown by an AbstractSecurityInterceptor, which is the main provider of authorization services. We will discuss AbstractSecurityInterceptor in the next section, but for now we just need to know that it produces Java exceptions and knows nothing about HTTP or how to go about authenticating a principal. Instead the ExceptionTranslationFilter offers this service, with specific responsibility for either returning error code 403 (if the principal has been authenticated and therefore simply lacks sufficient access - as per step seven above), or launching an AuthenticationEntryPoint (if the principal has not been authenticated and therefore we need to go commence step three).AuthenticationEntryPointThe AuthenticationEntryPoint is responsible for step three in the above list. As you can imagine, each web application will have a default authentication strategy (well, this can be configured like nearly everything else in Spring Security, but let’s keep it simple for now). Each major authentication system will have its own AuthenticationEntryPoint implementation, which typically performs one of the actions described in step 3.Authentication MechanismOnce your browser submits your authentication credentials (either as an HTTP form post or HTTP header) there needs to be something on the server that “collects” these authentication details. By now we’re at step six in the above list. In Spring Security we have a special name for the function of collecting authentication details from a user agent (usually a web browser), referring to it as the “authentication mechanism”. Examples are form-base login and Basic authentication. Once the authentication details have been collected from the user agent, an Authentication “request” object is built and then presented to the AuthenticationManager.After the authentication mechanism receives back the fully-populated Authentication object, it will deem the request valid, put the Authentication into the SecurityContextHolder, and cause the original request to be retried (step seven above). If, on the other hand, the AuthenticationManager rejected the request, the authentication mechanism will ask the user agent to retry (step two above).Storing the SecurityContext between requestsDepending on the type of application, there may need to be a strategy in place to store the security context between user operations. In a typical web application, a user logs in once and is subsequently identified by their session Id. The server caches the principal information for the duration session. In Spring Security, the responsibility for storing the SecurityContext between requests falls to the SecurityContextPersistenceFilter, which by default stores the context as an HttpSession attribute between HTTP requests. It restores the context to the SecurityContextHolder for each request and, crucially, clears the SecurityContextHolder when the request completes. You shouldn’t interact directly with the HttpSession for security purposes. There is simply no justification for doing so - always use the SecurityContextHolder instead.Many other types of application (for example, a stateless RESTful web service) do not use HTTP sessions and will re-authenticate on every request. However, it is still important that the SecurityContextPersistenceFilter is included in the chain to make sure that the SecurityContextHolder is cleared after each request.In an application which receives concurrent requests in a single session, the same SecurityContext instance will be shared between threads. Even though a ThreadLocal is being used, it is the same instance that is retrieved from the HttpSession for each thread. This has implications if you wish to temporarily change the context under which a thread is running. If you just use SecurityContextHolder.getContext(), and call setAuthentication(anAuthentication) on the returned context object, then the Authentication object will change in all concurrent threads which share the same SecurityContext instance. You can customize the behaviour of SecurityContextPersistenceFilter to create a completely new SecurityContext for each request, preventing changes in one thread from affecting another. Alternatively you can create a new instance just at the point where you temporarily change the context. The method SecurityContextHolder.createEmptyContext() always returns a new context instance. ...

February 21, 2019 · 5 min · jiezi

Spring Security整合KeyCloak保护Rest API

今天我们尝试Spring Security整合Keycloak,并决定建立一个非常简单的Spring Boot微服务,使用Keycloak作为我的身份验证源,使用Spring Security处理身份验证和授权。设置Keycloak首先我们需要一个Keycloak实例,让我们启动Jboss提供的Docker容器:docker run -d \ –name springboot-security-keycloak-integration \ -e KEYCLOAK_USER=admin \ -e KEYCLOAK_PASSWORD=admin \ -p 9001:8080 \ jboss/keycloak在此之后,我们只需登录到容器并导航到bin文件夹。docker exec -it springboot-security-keycloak-integration /bin/bashcd keycloak/bin首先,我们需要从CLI客户端登录keycloak服务器,之后我们不再需要身份验证:./kcadm.sh config credentials –server http://localhost:8080/auth –realm master –user admin –password admin配置realm首先,我们需要创建一个realm:./kcadm.sh create realms -s realm=springboot-security-keycloak-integration -s enabled=trueCreated new realm with id ‘springboot-security-keycloak-integration’之后,我们需要创建2个客户端,这将为我们的应用程序提供身份验证。首先我们创建一个cURL客户端,这样我们就可以通过命令行命令登录:./kcadm.sh create clients -r springboot-security-keycloak-integration -s clientId=curl -s enabled=true -s publicClient=true -s baseUrl=http://localhost:8080 -s adminUrl=http://localhost:8080 -s directAccessGrantsEnabled=trueCreated new client with id ‘8f0481cd-3bbb-4659-850f-6088466a4d89’重要的是要注意2个选项:publicClient=true和 directAccessGrantsEnabled=true。第一个使这个客户端公开,这意味着我们的cURL客户端可以在不提供任何秘密的情况下启动登录。第二个使我们能够使用用户名和密码直接登录。其次,我们创建了一个由REST服务使用的客户端:./kcadm.sh create clients -r springboot-security-keycloak-integration -s clientId=springboot-security-keycloak-integration-client -s enabled=true -s baseUrl=http://localhost:8080 -s bearerOnly=trueCreated new client with id ‘ab9d404e-6d5b-40ac-9bc3-9e2e26b68213’这里的重要配置是bearerOnly=true。这告诉Keycloak客户端永远不会启动登录过程,但是当它收到Bearer令牌时,它将检查所述令牌的有效性。我们应该注意保留这些ID,因为我们将在接下来的步骤中使用它们。我们有两个客户端,接下来是为spring-security-keycloak-example-app客户创建角色Admin Role:./kcadm.sh create clients/ab9d404e-6d5b-40ac-9bc3-9e2e26b68213/roles -r springboot-security-keycloak-integration -s name=admin -s ‘description=Admin role’Created new role with id ‘admin’User Role:./kcadm.sh create clients/ab9d404e-6d5b-40ac-9bc3-9e2e26b68213/roles -r springboot-security-keycloak-integration -s name=user -s ‘description=User role’Created new role with id ‘user’注意client后的id是我们创建客户端输出的id最后,我们应该获取客户端的配置,以便稍后提供给我们的应用程序:./kcadm.sh get clients/ab9d404e-6d5b-40ac-9bc3-9e2e26b68213/installation/providers/keycloak-oidc-keycloak-json -r springboot-security-keycloak-integration注意client后的id是我们创建客户端输出的id应该返回类似于此的内容:{ “realm” : “springboot-security-keycloak-integration”, “bearer-only” : true, “auth-server-url” : “http://localhost:8080/auth”, “ssl-required” : “external”, “resource” : “springboot-security-keycloak-integration-client”, “verify-token-audience” : true, “use-resource-role-mappings” : true, “confidential-port” : 0}配置用户出于演示目的,我们创建2个具有2个不同角色的用户,以便我们验证授权是否有效。首先,让我们创建一个具有admin角色的用户:创建admin用户:./kcadm.sh create users -r springboot-security-keycloak-integration -s username=admin -s enabled=trueCreated new user with id ‘50c11a76-a8ff-42b1-80cb-d82cb3e7616d’设置admin密码:./kcadm.sh update users/50c11a76-a8ff-42b1-80cb-d82cb3e7616d/reset-password -r springboot-security-keycloak-integration -s type=password -s value=admin -s temporary=false -nvalue: 用户密码追加到admin角色中./kcadm.sh add-roles -r springboot-security-keycloak-integration –uusername=admin –cclientid springboot-security-keycloak-integration-client –rolename admin注意:从不在生产中使用此方法,它仅用于演示目的!然后我们创建另一个用户,这次有角色user:创建user用户:./kcadm.sh create users -r springboot-security-keycloak-integration -s username=user -s enabled=trueCreated new user with id ‘624434c8-bce4-4b5b-b81f-e77304785803’设置user密码:./kcadm.sh update users/624434c8-bce4-4b5b-b81f-e77304785803/reset-password -r springboot-security-keycloak-integration -s type=password -s value=admin -s temporary=false -n追加到user角色中:./kcadm.sh add-roles -r springboot-security-keycloak-integration –uusername=user –cclientid springboot-security-keycloak-integration-client –rolename userRest服务我们已经配置了Keycloak并准备使用,我们只需要一个应用程序来使用它!所以我们创建一个简单的Spring Boot应用程序。我会在这里使用maven构建项目:<project xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xmlns=“http://maven.apache.org/POM/4.0.0" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.edurt.sski</groupId> <artifactId>springboot-security-keycloak-integration</artifactId> <packaging>jar</packaging> <version>1.0.0</version> <name>springboot security keycloak integration</name> <description>SpringBoot Security KeyCloak Integration is a open source springboot, spring security, keycloak integration example. </description> <properties> <!– dependency config –> <dependency.lombox.version>1.16.16</dependency.lombox.version> <dependency.springboot.common.version>1.5.6.RELEASE</dependency.springboot.common.version> <dependency.keycloak.version>3.1.0.Final</dependency.keycloak.version> <!– plugin config –> <plugin.maven.compiler.version>3.3</plugin.maven.compiler.version> <plugin.maven.javadoc.version>2.10.4</plugin.maven.javadoc.version> <!– environment config –> <environment.compile.java.version>1.8</environment.compile.java.version> <!– reporting config –> <reporting.maven.jxr.version>2.5</reporting.maven.jxr.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${dependency.springboot.common.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <!– lombok –> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${dependency.lombox.version}</version> </dependency> <!– springboot –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!– keycloak –> <dependency> <groupId>org.keycloak</groupId> <artifactId>keycloak-spring-boot-starter</artifactId> <version>${dependency.keycloak.version}</version> </dependency> <dependency> <groupId>org.keycloak</groupId> <artifactId>keycloak-spring-security-adapter</artifactId> <version>${dependency.keycloak.version}</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>${plugin.maven.compiler.version}</version> <configuration> <source>${environment.compile.java.version}</source> <target>${environment.compile.java.version}</target> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-javadoc-plugin</artifactId> <version>${plugin.maven.javadoc.version}</version> <configuration> <aggregate>true</aggregate> <!– custom tags –> <tags> <tag> <name>Description</name> <placement>test</placement> <head>description</head> </tag> </tags> <!– close jdoclint check document –> <additionalparam>-Xdoclint:none</additionalparam> </configuration> </plugin> </plugins> </build> <reporting> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jxr-plugin</artifactId> <version>${reporting.maven.jxr.version}</version> </plugin> </plugins> </reporting></project>添加所有必需的依赖项:spring-security 用于保护应用程序keycloak-spring-boot-starter 使用Keycloak和Spring Bootkeycloak-spring-security-adapter 与Spring Security集成一个简单的应用类:/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * “License”); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an “AS IS” BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. /package com.edurt.sski;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;/* * <p> SpringBootSecurityKeyCloakIntegration </p> * <p> Description : SpringBootSecurityKeyCloakIntegration </p> * <p> Author : qianmoQ </p> * <p> Version : 1.0 </p> * <p> Create Time : 2019-02-18 14:45 </p> * <p> Author Email: <a href=“mailTo:shichengoooo@163.com”>qianmoQ</a> </p> /@SpringBootApplicationpublic class SpringBootSecurityKeyCloakIntegration { public static void main(String[] args) { SpringApplication.run(SpringBootSecurityKeyCloakIntegration.class, args); }}Rest API接口:/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * “License”); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an “AS IS” BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. /package com.edurt.sski.controller;import org.springframework.security.access.annotation.Secured;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;/* * <p> HelloController </p> * <p> Description : HelloController </p> * <p> Author : qianmoQ </p> * <p> Version : 1.0 </p> * <p> Create Time : 2019-02-18 14:50 </p> * <p> Author Email: <a href=“mailTo:shichengoooo@163.com”>qianmoQ</a> </p> /@RestControllerpublic class HelloController { @GetMapping(value = “/admin”) @Secured(“ROLE_ADMIN”) public String admin() { return “Admin”; } @GetMapping("/user”) @Secured(“ROLE_USER”) public String user() { return “User”; }}最后是keycloak配置:/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * “License”); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an “AS IS” BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. /package com.edurt.sski.config;import org.keycloak.adapters.KeycloakConfigResolver;import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;import org.keycloak.adapters.springsecurity.filter.KeycloakAuthenticationProcessingFilter;import org.keycloak.adapters.springsecurity.filter.KeycloakPreAuthActionsFilter;import org.springframework.boot.web.servlet.FilterRegistrationBean;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;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.core.authority.mapping.GrantedAuthoritiesMapper;import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;/* * <p> KeycloakSecurityConfigurer </p> * <p> Description : KeycloakSecurityConfigurer </p> * <p> Author : qianmoQ </p> * <p> Version : 1.0 </p> * <p> Create Time : 2019-02-18 14:51 </p> * <p> Author Email: <a href=“mailTo:shichengoooo@163.com”>qianmoQ</a> </p> */@Configuration@EnableWebSecuritypublic class KeycloakSecurityConfigurer extends KeycloakWebSecurityConfigurerAdapter { @Bean public GrantedAuthoritiesMapper grantedAuthoritiesMapper() { SimpleAuthorityMapper mapper = new SimpleAuthorityMapper(); mapper.setConvertToUpperCase(true); return mapper; } @Override protected KeycloakAuthenticationProvider keycloakAuthenticationProvider() { final KeycloakAuthenticationProvider provider = super.keycloakAuthenticationProvider(); provider.setGrantedAuthoritiesMapper(grantedAuthoritiesMapper()); return provider; } @Override protected void configure(final AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(keycloakAuthenticationProvider()); } @Override protected SessionAuthenticationStrategy sessionAuthenticationStrategy() { return new NullAuthenticatedSessionStrategy(); } @Override protected void configure(final HttpSecurity http) throws Exception { super.configure(http); http .authorizeRequests() .antMatchers("/admin”).hasRole(“ADMIN”) .antMatchers("/user”).hasRole(“USER”) .anyRequest().permitAll(); } @Bean KeycloakConfigResolver keycloakConfigResolver() { return new KeycloakSpringBootConfigResolver(); } @Bean public FilterRegistrationBean keycloakAuthenticationProcessingFilterRegistrationBean( final KeycloakAuthenticationProcessingFilter filter) { final FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter); registrationBean.setEnabled(false); return registrationBean; } @Bean public FilterRegistrationBean keycloakPreAuthActionsFilterRegistrationBean( final KeycloakPreAuthActionsFilter filter) { final FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter); registrationBean.setEnabled(false); return registrationBean; }}KeycloakSecurityConfigurer类扩展 KeycloakWebSecurityConfigurerAdapter,这是Keycloak提供的类,它提供与Spring Security的集成。然后我们通过添加SimpleAuthorityMapper配置身份验证管理器,它负责转换来自Keycloak的角色名称以匹配Spring Security的约定。基本上Spring Security期望以ROLE_前缀开头的角色,ROLE_ADMIN可以像Keycloak一样命名我们的角色,或者我们可以将它们命名为admin,然后使用此映射器将其转换为大写并添加必要的ROLE_前缀:@Beanpublic GrantedAuthoritiesMapper grantedAuthoritiesMapper() { SimpleAuthorityMapper mapper = new SimpleAuthorityMapper(); mapper.setConvertToUpperCase(true); return mapper;}@Overrideprotected KeycloakAuthenticationProvider keycloakAuthenticationProvider() { final KeycloakAuthenticationProvider provider = super.keycloakAuthenticationProvider(); provider.setGrantedAuthoritiesMapper(grantedAuthoritiesMapper()); return provider;}@Overrideprotected void configure(final AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(keycloakAuthenticationProvider());}我们还需要为Keycloak设置会话策略,但是当我们创建无状态REST服务时,我们并不真的想要有会话,因此我们使用NullAuthenticatedSessionStrategy:@Overrideprotected SessionAuthenticationStrategy sessionAuthenticationStrategy() { return new NullAuthenticatedSessionStrategy();}通常,Keycloak Spring Security集成从keycloak.json文件中解析keycloak配置,但是我们希望有适当的Spring Boot配置,因此我们使用Spring Boot覆盖配置解析器:@BeanKeycloakConfigResolver keycloakConfigResolver() { return new KeycloakSpringBootConfigResolver();}然后我们配置Spring Security来授权所有请求:@Overrideprotected void configure(final HttpSecurity http) throws Exception { super.configure(http); http .authorizeRequests() .anyRequest().permitAll();}最后,根据文档,我们阻止双重注册Keycloak的过滤器:@Beanpublic FilterRegistrationBean keycloakAuthenticationProcessingFilterRegistrationBean( final KeycloakAuthenticationProcessingFilter filter) { final FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter); registrationBean.setEnabled(false); return registrationBean;}@Beanpublic FilterRegistrationBean keycloakPreAuthActionsFilterRegistrationBean( final KeycloakPreAuthActionsFilter filter) { final FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter); registrationBean.setEnabled(false); return registrationBean;}最后,我们需要application.properties使用之前下载的值配置我们的应用程序 :server.port=9002keycloak.realm=springboot-security-keycloak-integrationkeycloak.bearer-only=truekeycloak.auth-server-url=http://localhost:9001/authkeycloak.ssl-required=externalkeycloak.resource=springboot-security-keycloak-integration-clientkeycloak.use-resource-role-mappings=truekeycloak.principal-attribute=preferred_username使用应用程序使用curl我们创建的客户端进行身份验证,以获取访问令牌:export TOKEN=curl -ss --data "grant_type=password&amp;client_id=curl&amp;username=admin&amp;password=admin" http://localhost:9001/auth/realms/springboot-security-keycloak-integration/protocol/openid-connect/token | jq -r .access_token这将收到的访问令牌存储在TOKEN变量中。现在我们可以检查我们的管理员是否可以访问自己的/admin接口curl -H “Authorization: bearer $TOKEN” http://localhost:9002/adminAdmin但它无法访问/user接口:$ curl -H “Authorization: bearer $TOKEN” http://localhost:9002/user{“timestamp”:1498728302626,“status”:403,“error”:“Forbidden”,“message”:“Access is denied”,“path”:"/user"}对于user用户也是如此,user用户无法访问admin接口。源码地址:GitHub ...

February 18, 2019 · 6 min · jiezi

SpringSecurity登录使用JSON格式数据

在使用SpringSecurity中,大伙都知道默认的登录数据是通过key/value的形式来传递的,默认情况下不支持JSON格式的登录数据,如果有这种需求,就需要自己来解决,本文主要和小伙伴来聊聊这个话题。 Java通关秘笈小程序,视频教程、学习资料、重点知识一网打尽,你值得拥有!基本登录方案在说如何使用JSON登录之前,我们还是先来看看基本的登录吧,本文为了简单,SpringSecurity在使用中就不连接数据库了,直接在内存中配置用户名和密码,具体操作步骤如下:创建Spring Boot工程首先创建SpringBoot工程,添加SpringSecurity依赖,如下:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency>添加Security配置创建SecurityConfig,完成SpringSecurity的配置,如下:@Configurationpublic class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication().withUser(“zhangsan”).password("$2a$10$2O4EwLrrFPEboTfDOtC0F.RpUMk.3q3KvBHRx7XXKUMLBGjOOBs8q").roles(“user”); } @Override public void configure(WebSecurity web) throws Exception { } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .loginProcessingUrl("/doLogin") .successHandler(new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException { RespBean ok = RespBean.ok(“登录成功!",authentication.getPrincipal()); resp.setContentType(“application/json;charset=utf-8”); PrintWriter out = resp.getWriter(); out.write(new ObjectMapper().writeValueAsString(ok)); out.flush(); out.close(); } }) .failureHandler(new AuthenticationFailureHandler() { @Override public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException { RespBean error = RespBean.error(“登录失败”); resp.setContentType(“application/json;charset=utf-8”); PrintWriter out = resp.getWriter(); out.write(new ObjectMapper().writeValueAsString(error)); out.flush(); out.close(); } }) .loginPage("/login”) .permitAll() .and() .logout() .logoutUrl("/logout") .logoutSuccessHandler(new LogoutSuccessHandler() { @Override public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException { RespBean ok = RespBean.ok(“注销成功!”); resp.setContentType(“application/json;charset=utf-8”); PrintWriter out = resp.getWriter(); out.write(new ObjectMapper().writeValueAsString(ok)); out.flush(); out.close(); } }) .permitAll() .and() .csrf() .disable() .exceptionHandling() .accessDeniedHandler(new AccessDeniedHandler() { @Override public void handle(HttpServletRequest req, HttpServletResponse resp, AccessDeniedException e) throws IOException, ServletException { RespBean error = RespBean.error(“权限不足,访问失败”); resp.setStatus(403); resp.setContentType(“application/json;charset=utf-8”); PrintWriter out = resp.getWriter(); out.write(new ObjectMapper().writeValueAsString(error)); out.flush(); out.close(); } }); }}这里的配置虽然有点长,但是很基础,配置含义也比较清晰,首先提供BCryptPasswordEncoder作为PasswordEncoder,可以实现对密码的自动加密加盐,非常方便,然后提供了一个名为zhangsan的用户,密码是123,角色是user,最后配置登录逻辑,所有的请求都需要登录后才能访问,登录接口是/doLogin,用户名的key是username,密码的key是password,同时配置登录成功、登录失败以及注销成功、权限不足时都给用户返回JSON提示,另外,这里虽然配置了登录页面为/login,实际上这不是一个页面,而是一段JSON,在LoginController中提供该接口,如下:@RestController@ResponseBodypublic class LoginController { @GetMapping("/login") public RespBean login() { return RespBean.error(“尚未登录,请登录”); } @GetMapping("/hello") public String hello() { return “hello”; }}这里/login只是一个JSON提示,而不是页面, /hello则是一个测试接口。 OK,做完上述步骤就可以开始测试了,运行SpringBoot项目,访问/hello接口,结果如下: 此时先调用登录接口进行登录,如下: 登录成功后,再去访问/hello接口就可以成功访问了。使用JSON登录上面演示的是一种原始的登录方案,如果想将用户名密码通过JSON的方式进行传递,则需要自定义相关过滤器,通过分析源码我们发现,默认的用户名密码提取在UsernamePasswordAuthenticationFilter过滤器中,部分源码如下:public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { public static final String SPRING_SECURITY_FORM_USERNAME_KEY = “username”; public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = “password”; private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY; private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY; private boolean postOnly = true; public UsernamePasswordAuthenticationFilter() { super(new AntPathRequestMatcher("/login", “POST”)); } public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (postOnly && !request.getMethod().equals(“POST”)) { throw new AuthenticationServiceException( “Authentication method not supported: " + request.getMethod()); } String username = obtainUsername(request); String password = obtainPassword(request); if (username == null) { username = “”; } if (password == null) { password = “”; } username = username.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); // Allow subclasses to set the “details” property setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } protected String obtainPassword(HttpServletRequest request) { return request.getParameter(passwordParameter); } protected String obtainUsername(HttpServletRequest request) { return request.getParameter(usernameParameter); } //… //…}从这里可以看到,默认的用户名/密码提取就是通过request中的getParameter来提取的,如果想使用JSON传递用户名密码,只需要将这个过滤器替换掉即可,自定义过滤器如下:public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter { @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) { ObjectMapper mapper = new ObjectMapper(); UsernamePasswordAuthenticationToken authRequest = null; try (InputStream is = request.getInputStream()) { Map<String,String> authenticationBean = mapper.readValue(is, Map.class); authRequest = new UsernamePasswordAuthenticationToken( authenticationBean.get(“username”), authenticationBean.get(“password”)); } catch (IOException e) { e.printStackTrace(); authRequest = new UsernamePasswordAuthenticationToken( “”, “”); } finally { setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } } else { return super.attemptAuthentication(request, response); } }}这里只是将用户名/密码的获取方案重新修正下,改为了从JSON中获取用户名密码,然后在SecurityConfig中作出如下修改:@Overrideprotected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().authenticated() .and() .formLogin() .and().csrf().disable(); http.addFilterAt(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);}@BeanCustomAuthenticationFilter customAuthenticationFilter() throws Exception { CustomAuthenticationFilter filter = new CustomAuthenticationFilter(); filter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException { resp.setContentType(“application/json;charset=utf-8”); PrintWriter out = resp.getWriter(); RespBean respBean = RespBean.ok(“登录成功!”); out.write(new ObjectMapper().writeValueAsString(respBean)); out.flush(); out.close(); } }); filter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() { @Override public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException { resp.setContentType(“application/json;charset=utf-8”); PrintWriter out = resp.getWriter(); RespBean respBean = RespBean.error(“登录失败!”); out.write(new ObjectMapper().writeValueAsString(respBean)); out.flush(); out.close(); } }); filter.setAuthenticationManager(authenticationManagerBean()); return filter;}将自定义的CustomAuthenticationFilter类加入进来即可,接下来就可以使用JSON进行登录了,如下: 好了,本文就先介绍到这里,有问题欢迎留言讨论。 Java通关秘笈小程序,视频教程、学习资料、重点知识一网打尽,你值得拥有! ...

February 15, 2019 · 3 min · jiezi