关于spring-security:Spring-Security-–-RequestRejectedException

概述Spring Security 提供了 RequestRejectedHandler 来解决当申请被回绝时候如何解决,在没有进行配置的状况下,默认是应用 DefaultRequestRejectedHandler 间接将异样进行抛出: throw requestRejectedException;同时也提供了 HttpStatusRequestRejectedHandler 来返回对应的状态码。 定制 RequestRejectedHandlerRequestRejectedHandler 的注入是在 WebSecurity 的 setApplicationContext 当中: try { this.requestRejectedHandler = applicationContext.getBean(RequestRejectedHandler.class);}catch (NoSuchBeanDefinitionException ex) {}if (this.requestRejectedHandler != null) { filterChainProxy.setRequestRejectedHandler(this.requestRejectedHandler);}在中的定义为: private RequestRejectedHandler requestRejectedHandler = new DefaultRequestRejectedHandler();咱们只须要笼罩 Bean 定义即可: @BeanRequestRejectedHandler requestRejectedHandler() { return new CustomizerRequestRejectedHandler();}@Slf4jpublic class CustomizerRequestRejectedHandler implements RequestRejectedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, RequestRejectedException ex) throws IOException, ServletException { log.warn("request uri: {},user-agent: {}", request.getRequestURI(), request.getHeader(HttpHeaders.USER_AGENT)); log.error(ex.getMessage(), ex); response.setStatus(HttpServletResponse.SC_NOT_FOUND); }}参考spring-5-0-3-requestrejectedexception-the-request-was-rejected-because-the-url ...

January 5, 2023 · 1 min · jiezi

关于spring-security:Spring-Security怎么添加图片验证功能

前言Spring security增加图片验证形式,在互联网下面有很多这种博客,都写的十分的具体了。本篇次要讲一些增加图片验证的思路。还有前后端拆散形式,图片验证要怎么去解决? 本章内容图片验证的思路简略的demo思路小白: "咱们从总体流程上看图片验证在认证的哪一个阶段?" 小黑: "在获取客户输出的用户名明码那一阶段,而且要在服务器获取数据库中用户名明码之前。这是一个区间[获取申请用户名明码, 获取数据库用户名明码) 而在 Spring security中, 能够很显著的发现有两种思路。 第1种思路是在拦挡登录申请筹备认证的那个过滤器。第2种思路是在那个过滤器背地的认证器。"小白: "为什么是这个阶段呢? 不能是在判断明码验证之前呢?" 小黑: "你傻啊, 如果在你说的阶段, 服务器须要去数据库中获取用户信息, 这相当的节约系统资源" 小白: "哦哦, 我错了, 让我每每整个流程应该是啥样" 小白: "我须要当时在后端生成一个验证码,而后通过验证码返回一张图片给前端。前端登录表单增加图片验证。用户输出图片验证后点击登录,会寄存在request申请中, 后端须要从request申请中读取到图片验证,判断前后端验证码是否雷同, 如果图片验证码雷同之后才开始从数据库拿用户信息。否则间接抛出认证异样" 简略点: 数据库获取用户账户之前, 先进行图片验证码验证计划怎么将字符串变成图片验证码?这轮子必定不能自己造, 有就拿来吧你 kaptchahutoolkaptcha这么玩<!--验证码生成器--><dependency> <groupId>com.github.penggle</groupId> <artifactId>kaptcha</artifactId> <version>2.3.2</version> <exclusions> <exclusion> <artifactId>javax.servlet-api</artifactId> <groupId>javax.servlet</groupId> </exclusion> </exclusions></dependency>@Beanpublic DefaultKaptcha captchaProducer() { Properties properties = new Properties(); properties.put("kaptcha.border", "no"); properties.put("kaptcha.textproducer.char.length","4"); properties.put("kaptcha.image.height","50"); properties.put("kaptcha.image.width","150"); properties.put("kaptcha.obscurificator.impl","com.google.code.kaptcha.impl.ShadowGimpy"); properties.put("kaptcha.textproducer.font.color","black"); properties.put("kaptcha.textproducer.font.size","40"); properties.put("kaptcha.noise.impl","com.google.code.kaptcha.impl.NoNoise"); //properties.put("kaptcha.noise.impl","com.google.code.kaptcha.impl.DefaultNoise"); properties.put("kaptcha.textproducer.char.string","acdefhkmnprtwxy2345678"); DefaultKaptcha kaptcha = new DefaultKaptcha(); kaptcha.setConfig(new Config(properties)); return kaptcha;}@Resourceprivate DefaultKaptcha producer;@GetMapping("/verify-code")public void getVerifyCode(HttpServletResponse response, HttpSession session) throws Exception { response.setContentType("image/jpeg"); String text = producer.createText(); session.setAttribute("verify_code", text); BufferedImage image = producer.createImage(text); try (ServletOutputStream outputStream = response.getOutputStream()) { ImageIO.write(image, "jpeg", outputStream); }}hutool这么玩@GetMapping("hutool-verify-code")public void getHtoolVerifyCode(HttpServletResponse response, HttpSession session) throws IOException { CircleCaptcha circleCaptcha = CaptchaUtil.createCircleCaptcha(200, 100, 4, 80); session.setAttribute("hutool_verify_code", circleCaptcha.getCode()); response.setContentType(MediaType.IMAGE_PNG_VALUE); circleCaptcha.write(response.getOutputStream());}这俩轻易筛选一个完事前端就非常简单了 ...

January 3, 2023 · 9 min · jiezi

关于spring-security:重新认识Java微服务架构认证服务

前言之前通过浏览《Spring微服务实战》写过对于spring-cloud+spring-security+oauth2的认证服务和资源服务文章,以及写过对于spring-gateway做token校验的文章,然而在实战过程中还是发现一些问题,于是通过跟敌人沟通播种了不了新常识,之前的框架设计有问题,想通过这篇文章从新梳理下校验和认证流程。 遇到的问题1、Feign调用问题:之前所有微服务都做成了资源服务,这样feign调用的时候还要校验token,影响执行效率2、Gateway网关问题:spring-gateway校验了token并把token通过authorization做为申请头下发到上游微服务,上游服务又校验了一遍token,影响执行效率3、全局信息问题:如获取用户信息,微服务api接口通过OAuth2Authentication获取用户名,再通过UserService获取用户信息,这样做再次升高执行效率 如何去解决?综合下面三点问题,提出了绝对应的解决方案: 1、微服务不须要做成资源服务(不须要校验authorization),微服务的权限还有对立解决啥的都在网关里做,这样feign调用的时候也就不须要校验token了。2、下面说过微服务曾经不是资源服务,那么也不存在再次测验token的问题了,尽管如此,然而你能够通过spring-gateway来做对立受权达到管制外界的拜访。3、spring-gateway校验token和封装用户信息到申请头header中,上游服务通过header中的用户信息对立保留到Context中 留神:这里有个问题:A服务有个Controller办法叫saveUserEvent,feign通过/gateway-name/a/saveUserEvent路由调用(feign调用api接口的时候不存在token校验问题),然而没有了资源服务的token限度,里面当然也能够通过gateway调用这个接口,所以这里遇到的问题就是:如何既保证feign的顺利调用又不能让里面申请调用呢?针对这个问题答案是:通过gateway的路由黑名单把不想裸露给里面的api接口排除到路由里面,这样即保障了里面就再也申请不到这个接口了,又保障了服务内通过feign调用的数据安全性。 操作针对下面的三个问题,咱们来从新架构一下咱们的微服务。 1、spring-gateway认证服务操作流程cookie和spring-gateway联合做用户认证服务,这个是通过cookie和set-cookie来达到token传递的成果,前面独自写一篇文章解说。 2、封装用户信息到Context中留神:封装好的Context能够独自放到context包中,并且每个微服务都必须加。 如何来封装呢?其实我代码曾经写好了,大家能够参考应用。UserContext.java @Componentpublic class UserContext { public static final String CORRELATION_ID = "correlation-id"; public static final String AUTH_TOKEN = "authorization"; public static final String USER = "user"; private static final ThreadLocal<String> correlationId = new ThreadLocal<String>(); private static final ThreadLocal<String> authToken = new ThreadLocal<String>(); private static final ThreadLocal<LoginUser> user = new ThreadLocal<>(); public static String getCorrelationId() { return correlationId.get(); } public static void setCorrelationId(String cid) { correlationId.set(cid); } public static String getAuthToken() { return authToken.get(); } public static void setAuthToken(String token) { authToken.set(token); } public static LoginUser getUser() { return user.get(); } public static void setUser(LoginUser u) { user.set(u); } public static HttpHeaders getHttpHeaders() { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.set(CORRELATION_ID, getCorrelationId()); return httpHeaders; }}UserContextFilter.java ...

December 22, 2022 · 2 min · jiezi

关于spring-security:Spring-Security技术栈开发企业级认证与授权内附文档源码

Spring Security技术栈开发企业级认证与受权内附文档源码下载地址:百度网盘Java 诊断工具 Arthas-实操案例实操案例排查函数调用异样通过curl 请求接口只能看到返回异样,然而看不到具体的请求参数和堆栈信息。shell@Alicloud:~$ curl{"timestamp":1655435063042,"status":500,"error":"Internal Server Error","exception":"java.lang.IllegalArgumentException","message":"id < 1","path":"/user/0"}复制代码查看UserController的 参数/异样在Arthas里执行:watch com.example.demo.arthas.user.UserController * '{params, throwExp}'复制代码 第一个参数是类名,反对通配第二个参数是函数名,反对通配 拜访watch命令会打印调用的参数和异样再次通过curl 调用可能在arthas外面查看到具体的异样信息。把获取到的后果开展,可能用-x参数:watch com.example.demo.arthas.user.UserController * '{params, throwExp}' -x 2复制代码返回值表达式在下面的例子里,第三个参数是返回值表达式,它实际上是一个ognl表达式,它反对一些内置对象: loaderclazzmethodtargetparamsreturnObjthrowExpisBeforeisThrowisReturn 比如返回一个数组:watch com.example.demo.arthas.user.UserController * '{params[0], target, returnObj}'复制代码条件表达式watch命令反对在第4个参数里写条件表达式,比如:当拜访 user/1 时,watch命令没有输入当拜访 user/101 时,watch会打印出后果。 当异样时捕捉watch命令反对-e选项,示意只捕捉抛出异样时的请求:watch com.example.demo.arthas.user.UserController * "{params[0],throwExp}" -e复制代码按照耗时进行过滤watch命令反对按请求耗时进行过滤,比如:watch com.example.demo.arthas.user.UserController * '{params, returnObj}' '#cost>200'复制代码热更新代码这个也是真的秀。 拜访 http://localhost:61000/user/0 ,会返回500异样:shell@Alicloud:~$ curl http://localhost:61000/user/0{"timestamp":1655436218020,"status":500,"error":"Internal Server Error","exception":"java.lang.IllegalArgumentException","message":"id < 1","path":"/user/0"}复制代码通过热更新代码,修改这个逻辑。jad反编译UserControllerjad --source-only com.example.demo.arthas.user.UserController > /tmp/UserController.java复制代码jad反编译的后果保存在 /tmp/UserController.java文件里了。再打开一个Terminal 窗口,而后用vim来编辑/tmp/UserController.java:vim /tmp/UserController.java

August 16, 2022 · 1 min · jiezi

关于spring-security:Spring-Security-实现动态权限菜单方案附源码

零碎权限治理1、前言在理论开发中,开发任何一套零碎,根本都少不了权限治理这一块。这些足以阐明权限治理的重要性。其实SpringSecurity去年就学了,始终没有工夫整顿,用了一年多工夫了,给我的印象始终都挺好,实用,安全性高(Security能够对明码进行加密)。而且这一块在理论开发中也确实很重要,所以这里整顿了一套基于SpringSecurity的权限治理。 案例代码上面有下载链接。2、案例技术栈如果对于SpringSecurity还不理解的话能够先理解一下SpringSecurity平安控件的学习,页面采纳的是Bootstrap写的(页面就简略的写了一下,能够依据本人的需要更改),其实后端了解了,前台就是显示作用,大家能够自行更换前台页面显示框架,长久层应用的是Spring-Data-Jpa。 并且对后端长久层和控制器进行了一下小封装,Java长久层和控制器的封装。页面应用的Thymeleaf模板,SpringBoot整合Thymeleaf模板。 数据库设计1、表关系 菜单(TbMenu)=====> 页面上须要显示的所有菜单角色(SysRole)=====> 角色及角色对应的菜单用户(SysUser)=====> 用户及用户对应的角色用户和角色两头表(sys_user_role)====> 用户和角色两头表2、数据库表构造菜单表tb_menu 角色及菜单权限表sys_role,其中父节点parent 为null时为角色,不为null时为对应角色的菜单权限。 用户表sys_user 用户和角色多对多关系,用户和角色两头表sys_user_role(有Spring-Data-Jpa主动生成)。 新建我的项目1、新建springboot我的项目新建springboot我的项目,在我的项目中增加SpringSecurity相干Maven依赖,pom.map文件 <?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 https://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.2.2.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.mcy</groupId> <artifactId>springboot-security</artifactId> <version>0.0.1-SNAPSHOT</version> <name>springboot-security</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</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-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </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> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.webjars.bower</groupId> <artifactId>bootstrap-select</artifactId> <version>2.0.0-beta1</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>bootbox</artifactId> <version>4.4.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>2、我的项目构造 ...

July 6, 2022 · 6 min · jiezi

关于spring-security:springsecurity和ObjectMapperjson反序列化的白名单问题记录

这几天在弄一个对立的反对多租户的认证鉴权平台,就想到了应用oauth这种受权协定来进行,通过蕴含open-user信息的jwt来进行散发。应用这个遇到了一些坑,记录如下: 先阐明一下状况,遇到的是fasterxml的json反序列化的白名单问题,因为解决json的框架容易受到攻打,经常出现一些反序列化破绽,新出一个如果采纳黑名单,则这个名单会一直宏大,须要不断更新,故此fasterxml采纳了白名单的机制来实现json反序列化。 这个问题暴发在于应用了OAuth2AuthorizationService这个实现类,外面有new ObjectMapper();咱们不能管制它外面本人new的ObjectMapper,所以咱们必须从新定义一个OAuth2AuthorizationService, @Bean public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository, ObjectMapper objectMapper) { JdbcOAuth2AuthorizationService service = new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository); JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper authorizationRowMapper = new JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper(registeredClientRepository); authorizationRowMapper.setLobHandler(new DefaultLobHandler()); authorizationRowMapper.setObjectMapper(objectMapper); service.setAuthorizationRowMapper(authorizationRowMapper); return service; }这里注入ObjectMapper,咱们就能对其进行定制化了。 //@Bean 这里能够看状况全局配置objectMapper,如果用到了springmvc,会烦扰到springmvc的json解析//能够把这段写到OAuth2AuthorizationService的Bean定义外面去 public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() { return builder -> { //jackson反序列化 平安白名单 builder.mixIn(OpenAppUser.class, OpenAppUserMixin.class); List<Module> securityModules = SecurityJackson2Modules.getModules(LocalDateTimeSerializerConfig.class.getClassLoader()); securityModules.add(new OAuth2AuthorizationServerJackson2Module()); builder.modulesToInstall(securityModules.toArray(new Module[securityModules.size()])); }; }能够去看看这几个Module,外面都是security定义的一些退出平安白名单的类。这里我本人定义了一个用户类,因为登录查问的用户就是应用的这个这样就能够进行自定义用户对象的json平安反序列化了。 //实际上应用security和objectmapper的时候提供了三种形式来实现退出反序列化的白名单,在下面的那个加载Module代码里,首先都会调用SecurityJackson2Modules.enableDefaultTyping(mapper);//办法,而后外面调用mapper.setDefaultTyping(createAllowlistedDefaultTyping());//返回的是一个TypeResolverBuilder,这里的实现类是AllowlistTypeResolverBuilderprivate static TypeResolverBuilder<? extends TypeResolverBuilder> createAllowlistedDefaultTyping() { TypeResolverBuilder<? extends TypeResolverBuilder> result = new AllowlistTypeResolverBuilder( ObjectMapper.DefaultTyping.NON_FINAL); result = result.init(JsonTypeInfo.Id.CLASS, null); result = result.inclusion(JsonTypeInfo.As.PROPERTY); return result; }//AllowlistTypeResolverBuilder类外面的idResolver()办法返回的是TypeIdResolver,实现是一个AllowlistTypeIdResolver()@Overrideprotected TypeIdResolver idResolver(MapperConfig<?> config, JavaType baseType, PolymorphicTypeValidator subtypeValidator, Collection<NamedType> subtypes, boolean forSer, boolean forDeser) { TypeIdResolver result = super.idResolver(config, baseType,subtypeValidator, subtypes, forSer, forDeser); return new AllowlistTypeIdResolver(result);}//重点就是这个类了,它外面的typeFromId办法@Overridepublic JavaType typeFromId(DatabindContext context, String id) throws IOException { DeserializationConfig config = (DeserializationConfig) context.getConfig(); JavaType result = this.delegate.typeFromId(context, id); String className = result.getRawClass().getName(); if (isInAllowlist(className)) { return result; } boolean isExplicitMixin = config.findMixInClassFor(result.getRawClass()) != null; if (isExplicitMixin) { return result; } JacksonAnnotation jacksonAnnotation = AnnotationUtils.findAnnotation(result.getRawClass(), JacksonAnnotation.class); if (jacksonAnnotation != null) { eturn result; } throw new IllegalArgumentException("The class with " + id + " and name of " + className + " is not in the allowlist. " + "If you believe this class is safe to deserialize, please provide an explicit mapping using Jackson annotations or by providing a Mixin. " + "If the serialization is only done by a trusted source, you can also enable default typing. " + "See https://github.com/spring-projects/spring-security/issues/4370 for details");}能够看到security提供的这个办法外面的实现,三种形式别离是1.isInAllowlist()是否在这个白名单汇合里2.是否有退出到MixIn,这个就是咱们后面应用的形式3.反序列化类上是否有@JacksonAnnotation注解对于形式1,是个static汇合不能批改;其余两种都能够 ...

November 11, 2021 · 2 min · jiezi

关于spring-security:springsecurity的认证鉴权acloauth20

次要就是一些拦截器链,@PreAuthorize,@PreFilter,@PostAuthorize和@PostFilter 认证AuthenticationManager 基于列表的ProviderManager实现,每个处理器都有机会解决验证胜利或失败 AuthenticationProvider获取适配的处理器 鉴权AccessDecisionManager 基于投票的 AccessDecisionManager 实现,投票决策管理器AccessDecisionVoter 根本实现: RoleVoter投票ROLE_结尾的权限 AuthenticatedVoter用于辨别匿名、齐全身份验证和记住我身份验证的用户 RoleHierarchyVoter层级角色,容许您配置哪些角色admin(或权限)应该包含其余角色user(或权限) AfterInvocationManager 调用后处理,批改平安对象调用理论返回的对象的办法 acl(域对象)访问控制列表 能够应用PermissionEvaluator接口 基于表达式的访问控制 hasPermission(target, permission) boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission);第一种用于拜访被管制的域对象曾经加载的状况。 boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission);第二个版本用于未加载对象但其标识符已知的状况。 旨在表达式零碎和 Spring Security 的 ACL 零碎之间架起桥梁,容许您依据形象权限指定对域对象的受权束缚。它对 ACL 模块没有显式依赖,因而您能够依据须要将其替换为代替实现。 在spring-security-acl.jar里有建表语句,能够开箱即用AclService,AclEntryVoter等类 oauth2协定资源服务器和资源受权服务器 有两个我的项目,spring-security-oauth2和spring-security我的项目下的oauth2模块,目前spring把性能都迁徙到spring-security了,举荐应用spring-security。 spring-security的oauth2.0 客户端反对是通过oauth2Client() DSL(配置,fromLogin那里)办法实现的。资源服务器反对是通过 oauth2ResourceServer() DSL实现的,并且提供了一个OAuth2AuthorizedClientService开箱即用的类。受权服务器目前security还没有集成oauth2.0的,所以只能先用oauth2.0的,应用@EnableAuthorizationServer注解 提供了@RegisteredOAuth2AuthorizedClient注解,将受权的客户端存储在本人的OAuth2AuthorizedClientRepository。 ClientRegistrationRepository来示意客户端,能够通过 Spring Security DSL 提供。或者,这些也能够通过 Spring Boot 进行配置。 能够定义多个SecurityFilterChain,依据matches申请走不同的过滤器链 @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SecurityFilterChain systemSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); http.getConfigurer(OAuth2AuthorizationServerConfigurer.class) .authorizationEndpoint(config->{ OAuth2AuthorizationEndpointConfigurer configurer = (OAuth2AuthorizationEndpointConfigurer) config; }); return http.build(); } @Bean public SecurityFilterChain clientSecurityFilterChain(HttpSecurity http) throws Exception { http.authorizeRequests() .requestMatchers(requestMatchers).permitAll() .anyRequest().denyAll().and() .formLogin().and(); return http.build(); }

November 3, 2021 · 1 min · jiezi

关于spring-security:Growing-账号认证实践

背景GrowingIO 作为业余的数据经营解决方案提供商,咱们的客户来自不同的行业,但他们都有雷同的平安需要。在泛滥的客户中,许多客户都有本人的账号认证零碎。因而咱们须要能通过简略的配置接入客户的账号认证零碎。目前 GrowingIO 一共反对了 CAS, OAuth2, LDAP 三种不同的接入协定。本文将具体介绍咱们是如何反对这三个接入形式的。 不同接入协定的认证流程OAuth2一般来说,应用 OAuth2 来实现认证都是应用的受权码模式,咱们这里也不例外,上面是 OAuth2 受权码模式的规范流程。 (A)用户拜访客户端,后者将前者导向认证服务器。 (B)用户抉择是否给予客户端受权。 (C)假如用户给予受权,认证服务器将用户导向客户端当时指定的"重定向URI"(redirection URI),同时附上一个受权码。 (D)客户端收到受权码,附上新近的"重定向 URI ",向认证服务器申请令牌。这一步是在客户端的后盾的服务器上实现的,对用户不可见。 (E)认证服务器核查了受权码和重定向URI,确认无误后,向客户端发送拜访令牌(access token)和更新令牌( refresh token)。 LDAPLDAP 全称 Lightweight Directory Access Protocol,中文名称轻量目录拜访协定。认证登录是其次要利用场景之一,上面是 LDAP 认证的流程。 CASCAS 是一个开源的 Java 服务器组件,给企业提供 Web 单点登录服务,CAS 服务器是构建在 Spring Framework 上的 Java 应用程序,其主要职责是通过颁发和验证票证来验证用户并授予对启用 CAS 的服务(通常称为 CAS 客户端)的拜访权限。当服务器在胜利登录后向用户收回票据授予票据 (TGT) 时,将创立 SSO 会话。服务票证 (ST) 依据用户的申请通过浏览器重定向应用 TGT 作为令牌颁发给服务。ST 随后在 CAS 服务器上通过反向通道通信进行验证。 整体架构 总体思路整体上就是把这三种认证形式都集成到了 OAuth2 受权码模式的流程中,认证核心 IAM 零碎在对接不同的认证协定时表演了不同的角色。 对于账号密码认证, IAM 表演的是 OAuth Server,用户的信息保留在数据库 users 表中,网关Gateway 表演的是 OAuth Client,走的是规范的受权码流程。对于 LDAP 认证,IAM 表演的是 LDAP Client,在认证过程中把用户的用户名和明码拿到 LDAP Server 进行查问,如果查问到非法用户则代表认证胜利,之后再走后续的受权码流程。对于 CAS 认证,IAM表演的是 CAS Client,在获取受权码的接口解决逻辑中,IAM 会检查用户是否认证过,如果没有认证过,会把用户重定向到 CAS Server 去进行认证。在 CAS 的认证回调接口中,依据 ticket 换取用户信息,之后设置用户的认证状态并且生成本人的受权码,前面就是规范的受权码流程。对于 OAuth2 认证,IAM 表演的是 OAuth Client,和 CAS 认证的思路雷同,发现用户没有认证时把用户重定向到 OAuth Server 去进行认证或者受权。在 Oauth Server 的受权码回调接口中,依据返回的受权码拿到 token,进而拿到用户信息,之后设置用户的认证状态并且生成本人的受权码,前面就是规范的受权码流程。登录流程账号密码 & LDAP ...

September 18, 2021 · 3 min · jiezi

关于spring-security:Eurynome-Cloud-Athena-基于Spring-Security-OAuth2-的前后端分离脚手架

Eurynome Cloud Athena 是什么?Eurynome Cloud Athena 是从 Eurynome Cloud 中提取进去的、能够独立运行的、基于OAuth2认证的、前后端拆散的单体式后盾治理脚手架。Eurynome Cloud Athena 实质上就是eurynome-cloud-oauth-starter的利用。从pom中能够看到该工程外围依赖就是Eurynome Cloud 中eurynome-cloud-oauth-starter。Eurynome Cloud Athena 更多的是一个演示性工程,用来示例如何应用eurynome-cloud-oauth-starter,以及相干的配置参数是如何配置的。也能够齐全不必照搬Athena工程,本人新建一个Spring Boot工程,增加eurynome-cloud-oauth-starter依赖和相应的配置也能够应用。Eurynome Cloud Athena 不是什么?Eurynome Cloud Athena 并不是一个残缺的开源我的项目,其外围代码eurynome-cloud-oauth-starter,须要通过编译Eurynome Cloud生成(目前 Eurynome Cloud 外围包并没有提交至Maven地方仓库)。 为什么 Eurynome Cloud Athena 和 Eurynome Cloud 共享代码?不论是独自搭建基于Spring Security OAuth2的后盾治理脚手架,还是构建基于Spring Cloud的散布式微服务架构,Spring Security和OAuth2外围代码的利用形式都是不变的,能够是通用的,因而将Security和OAuth2以及其它通用的代码放在Eurynome Cloud中,编译后供Eurynome Cloud Athena应用。 已经也思考过,在从新提取一个工程,专门搁置通用代码。然而这种形式,额定多了一道编译手续,也不便于对Eurynome Cloud整个代码的理解和应用,因而放弃了这种形式。 为什么构建 Eurynome Cloud Athena?基于Spring Cloud和Spring Cloud Alibaba的微服务架构,曾经成为利用建设的支流计划。然而不可否认的是,搭建一套微服务架构所需的基础架构越来越多,也越来越简单,所需的配套资源也越来越大。仅仅是在开发电脑上搭建一套运行开发调试环境,其复杂度和所需的资源也不容小觑。而很多利用,特地是小型利用,在晚期开发中或者用户量不大的后期齐全没有必要上一整套微服务,额定减少复杂度。很多状况下一套单体的、前后端拆散的后盾就足以满足。 因为以上的思考,才构建的Eurynome Cloud Athena。其实只有Spring Security和OAuth2外围代码写的足够通用,单体式架构就自然而然的产生了。 Eurynome Cloud Athena 不须要搭建Nacos、ELK、Sentinel、Skywalking等基础设施,只有一个数据库就能够独立运行,而且具备微服务架构除服务治理以外的所有性能。不仅编译和运行速度有几倍的晋升,而且只有代码标准、分包正当,能够疾速无缝迁徙到微服务架构。这有助于在我的项目晚期疾速建设项目,不便开发人员在本地进行开发以及技术钻研。 如果你没有大量的工夫和资源搭建微服务架构,那么就能够尝试应用Eurynome Cloud Athena,能够从另一个角度疾速、全面地理解Eurynome Cloud。 我的项目地址后端Gitee地址:https://gitee.com/herodotus/eurynome-cloud后端Github地址:https://github.com/herodotus-cloud/eurynome-cloud

June 30, 2021 · 1 min · jiezi

关于spring-security:记录一次将数据库中密码升级为密文过程

前言改一个老我的项目,老我的项目原来存储明码用的明文,要升级成密文。大略分子工作有两个,一是存储明码时加密,一是将原来数据库中的明码加密。 应用BCryptPasswordEncoder加密BCryptPasswordEncoder是springboot 我的项目中最罕用的明码加密形式,由spring security向咱们提供。咱们首先在配置文件下引入spring security <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency>然而这也会导致一个问题,如果引入这个包的话就会主动启用spring security,再申请的时候就须要用户认证,未认证的用户申请会报403。然而老我的项目并没有应用spring security,而是本人写的登录,手动验证用户名明码。所以我须要敞开spring security作用。在网上查了一下,解决办法是通过在启动类上增加正文去疏忽security无关类。 @SpringBootApplication(exclude = { SecurityAutoConfiguration.class })这个解决形式还有来的及验证,首先在单元测试有问题了。单元测试在测试登录c层时,报错403,这必定是因为security认证没有通过,解决办法是通过在@AutoConfigureMockMvc上退出参数 @AutoConfigureMockMvc(addFilters = false)@AuthConfigureMockMvc负责依赖注入mockMvc,而咱们应用mockMvc进行模仿申请。spring security在校验时通过一条过滤器链,咱们减少addFilters = false使得过滤器不执行,从而不进行认证。这个问题解决了,他的子测试类又报问题了。 @Test public void getById() throws Exception { logger.info("新增一个学院"); College college = collegeService.getOneSavedCollege(); logger.info("获取新增学院,断言胜利"); Long id = college.getId(); mockMvc.perform(get(baseUrl + "/" + id) .cookie(this.cookie)) .andExpect(status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(id)); }他在第7行的mockMvc.perform()报了java.lang.NullPointerException异样,打断点发现是cookie为null,这就是父类通过登录获取cookie,子类通过cookie进行身份认证。然而当初通过测试发现咱们禁用过滤器获取不到cookie了。咱们禁用过滤器是不想让security起作用,然而也造成了获取不到cookie了。我尝试应用其余办法解决禁用security的问题。通过网上给的办法,如退出@WithMockUser,都没有起成果,申请还是报错403。 我只想要BCryptPasswordEncoder,却因为引入他所在的包产生了负影响。询问老师后,老师给出倡议能不能只引入BCryptPasswordEncoder包,在网上找了一番并没有这个包。老师说把BCryptPasswordEncoder类代码复制下来应用,BCryptPasswordEncoder类并没有依赖其余类。引入后,能够失常应用,问题解决。 将生产环境的用户明码明文变密文因为我的项目在生产环境曾经应用一段时间了,认为明码都变成密文了,判断明码形式也变了,咱们也要讲原来用的的明码变为密文,能力不影响用户登录。思路是将所有用户取出来而后明码加密一边而后存回去。要害是如何实现只执行一次。如果明码加密两次必定就不对了。参考老我的项目是通过一个数据库存储后盾版本,而后在启动时判断是否是此版本,如果不是,存储此版本并将明码加密。 @Override public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) { String version = "1.0.0"; if (!this.schemaHistoryRepository.findById(version).isPresent()) { this.schemaHistoryRepository.save(new SchemaHistory(version)); List<User> users = this.userRepository.findAll(); for (User user: users) { // 将未加密的数据库中的明码加密 user.setPassword(user.getPassword()); } this.userRepository.saveAll(users); } }BCryptPasswordEncoder在改写setPassword()办法的过程中遇到个问题,因为用户在存储明码时生成一个密文,而后用户登录时前台传给后盾json类型的username和password。后盾通过setPassword()为user赋值,这时前台传来的明码又加密了一编,而BCryptPasswordEncoder雷同数据两次加密后果不一样。导致不能通过判断加密后的明码验证明码谬误与否。这不得不重写一个setPasswordWithEncoder()办法去加密明码,setPassword()办法保留为了接管前台传来的password。这种解决办法使得改起来变得特地凌乱,改单元测试时不晓得用什么办法才对。老师倡议从新建设一个类用于接管前台传来的用户对象 ...

June 12, 2021 · 1 min · jiezi

关于spring-security:spring-security之Secured角色权限

前言写完后盾接口当前,剩下的是退出权限管制。最简略是对于spring security退出的@Secured注解,注解里退出容许拜访的角色即可。 @Secured("ROLE_VIEWER")public String getUsername() { SecurityContext securityContext = SecurityContextHolder.getContext(); return securityContext.getAuthentication().getName();}然而他里边的角色是如何与我定义的用户角色对应的呢,又是对应的哪个字段呢,我也没找到解释,想着写完当前去钻研一下。先写了一个尝试一下,在获取clazz分页数据接口上退出只能管理员拜访,而后登陆一个学生用户,用url跳转到clazz模块,发现起作用了。而后就将大部分接口都退出了@Secured注解。再启动我的项目就发现就不对劲了,他是起作用了,然而对所有角色用户都拦挡了。一开始想到写法不太正确,然而又不晓得外头填的角色名称跟什么绝对应。这能去钻研他的原理。 过程去网上找了很多材料,对于这方面的形容都只停留在应用层面。都是说你只有退出@Secured("ADMIN")注解,就只有你角色是ADMIN的用户能力拜访。然而怎么让用户角色是ADMIN,并没有具体的介绍。网上找不到材料,我问了学长对于原来我的项目的应用办法。学长通知我是因为实现UserDetailsService接口的loadUserByUsername办法里去增加获取到的user的角色。 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { logger.debug("依据用户名查问用户"); User user = this.userRepository.findByUsername(username); if (user == null) { logger.error("用户名不存在"); throw new UsernameNotFoundException("用户名不存在"); } logger.debug("获取用户受权菜单"); Set<Menu> menus = new HashSet<>(); for (Role role : user.getRoles()) { menus.addAll(role.getMenus()); } logger.debug("初始化受权列表"); List<SimpleGrantedAuthority> authorities = new ArrayList<>(); logger.debug("依据菜单进行 API 受权"); for (Menu menu : menus) { authorities.add(new SimpleGrantedAuthority(menu.getRoleName())); } logger.debug("结构用户"); return new YzUserDetail(user, username, user.getPassword(), authorities);}而后与@Secured注解里咱们定义的角色名称绝对应。我一看我的我的项目中loadUserByUsername办法向UserDetail办法传入的authorities参数是一个new ArrayList<>()空数组。我就试着变成传入角色数组,再去试验,还是将所有角色拦挡了。并没有解决问题。而后我在loadUserByUsername办法上打断点,发现看看是否执行,发现只有登录的时候执行了,然而是获取到用户角色了的。我就想可能是其余中央实现的传入角色。我去找哪还用了UserDetail。发现在咱们自定义的spring security过滤器中也使用了, ...

May 9, 2021 · 1 min · jiezi

关于spring-security:spring-security-oAuth2

@Controller与@RestController@Controller与@RestController的区别1 后者不能返回jsp页面,  @RestController是@Controller和@ResponseBody的结合体,两个标注合并起来的作用。2、如果只是应用@RestController注解Controller,则Controller中的办法无奈返回jsp页面,配置的视图解析器InternalResourceViewResolver不起作用,返回的内容就是Return 里的内容。 例如:原本应该到success.jsp页面的,则其显示success. 3、如果须要返回到指定页面,则须要用 @Controller配合视图解析器InternalResourceViewResolver才行。 4、如果须要返回JSON,XML或自定义mediaType内容到页面,则须要在对应的办法上加上@ResponseBody注解。

January 21, 2021 · 1 min · jiezi

关于spring-security:SpringBootSpringSecurityJWT整合实现单点登录SSO史上最全详解

欢送微信搜寻公众号【java版web我的项目】获取资源:java学习视频/设计模式笔记/算法手册/java我的项目 一、什么是单点登陆 单点登录(Single Sign On),简称为 SSO,是目前比拟风行的企业业务整合的解决方案之一。SSO的定义是在多个利用零碎中,用户只须要登录一次就能够拜访所有相互信任的利用零碎 二、简略的运行机制 单点登录的机制其实是比较简单的,用一个事实中的例子做比拟。某公园外部有许多独立的景点,游客能够在各个景点门口独自买票。对于须要玩耍所有的景点的游客,这种买票形式很不不便,须要在每个景点门口排队买票,钱包拿 进拿出的,容易失落,很不平安。于是绝大多数游客抉择在大门口买一张通票(也叫套票),就能够玩遍所有的景点而不须要从新再买票。他们只须要在每个景点门 口出示一下方才买的套票就可能被容许进入每个独立的景点。单点登录的机制也一样,如下图所示, 用户认证:这一环节次要是用户向认证服务器发动认证申请,认证服务器给用户返回一个胜利的令牌token,次要在认证服务器中实现,即图中的认证零碎,留神认证零碎只能有一个。身份校验:这一环节是用户携带token去拜访其余服务器时,在其余服务器中要对token的真伪进行测验,次要在资源服务器中实现,即图中的利用零碎2 3 三、JWT介绍概念阐明 从分布式认证流程中,咱们不难发现,这两头起最关键作用的就是token,token的平安与否,间接关系到零碎的健壮性,这里咱们抉择应用JWT来实现token的生成和校验。 JWT,全称JSON Web Token,官网地址https://jwt.io,是一款杰出的分布式身份校验计划。能够生成token,也能够解析测验token。 JWT生成的token由三局部组成:头部:次要设置一些标准信息,签名局部的编码格局就在头部中申明。载荷:token中寄存无效信息的局部,比方用户名,用户角色,过期工夫等,然而不要放明码,会泄露!签名:将头部与载荷别离采纳base64编码后,用“.”相连,再退出盐,最初应用头部申明的编码类型进行编码,就失去了签名。 JWT生成token的安全性剖析 从JWT生成的token组成上来看,要想防止token被伪造,次要就得看签名局部了,而签名局部又有三局部组成,其中头部和载荷的base64编码,简直是通明的,毫无安全性可言,那么最终守护token平安的重任就落在了退出的盐下面了!试想:如果生成token所用的盐与解析token时退出的盐是一样的。岂不是相似于中国人民银行把人民币防伪技术公开了?大家能够用这个盐来解析token,就能用来伪造token。这时,咱们就须要对盐采纳非对称加密的形式进行加密,以达到生成token与校验token方所用的盐不统一的平安成果! 非对称加密RSA介绍基本原理:同时生成两把密钥:私钥和公钥,私钥隐秘保留,公钥能够下发给信赖客户端私钥加密,持有私钥或公钥才能够解密公钥加密,持有私钥才可解密长处:平安,难以破解毛病:算法比拟耗时,为了平安,能够承受历史:三位数学家Rivest、Shamir 和 Adleman 设计了一种算法,能够实现非对称加密。这种算法用他们三个人的名字缩写:RSA。 四、SpringSecurity整合JWT1.认证思路剖析 SpringSecurity次要是通过过滤器来实现性能的!咱们要找到SpringSecurity实现认证和校验身份的过滤器! 回顾集中式认证流程用户认证: 应用UsernamePasswordAuthenticationFilter过滤器中attemptAuthentication办法实现认证性能,该过滤器父类中successfulAuthentication办法实现认证胜利后的操作。身份校验: 应用BasicAuthenticationFilter过滤器中doFilterInternal办法验证是否登录,以决定是否进入后续过滤器。 剖析分布式认证流程用户认证: 因为分布式我的项目,少数是前后端拆散的架构设计,咱们要满足能够承受异步post的认证申请参数,须要批改UsernamePasswordAuthenticationFilter过滤器中attemptAuthentication办法,让其可能接管申请体。 另外,默认successfulAuthentication办法在认证通过后,是把用户信息间接放入session就完事了,当初咱们须要批改这个办法,在认证通过后生成token并返回给用户。身份校验: 原来BasicAuthenticationFilter过滤器中doFilterInternal办法校验用户是否登录,就是看session中是否有用户信息,咱们要批改为,验证用户携带的token是否非法,并解析出用户信息,交给SpringSecurity,以便于后续的受权性能能够失常应用。 2.具体实现 为了演示单点登录的成果,咱们设计如下我的项目构造 2.1父工程创立 因为本案例须要创立多个零碎,所以咱们应用maven聚合工程来实现,首先创立一个父工程,导入springboot的父依赖即可 <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> <relativePath/></parent>1234562.2公共工程创立 而后创立一个common工程,其余工程依赖此零碎导入JWT相干的依赖 <dependencies> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.10.7</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.10.7</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.10.7</version> <scope>runtime</scope> </dependency> <!--jackson包--> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.9.9</version> </dependency> <!--日志包--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </dependency> <dependency> <groupId>joda-time</groupId> <artifactId>joda-time</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency></dependencies>123456789101112131415161718192021222324252627282930313233343536373839404142创立相干的工具类 ...

January 6, 2021 · 9 min · jiezi

关于spring-security:Spring-Cloud-Spring-Security实践五-整合验证码功能

此处应用kaptcha依赖实现图形验证码校验性能 引入依赖<dependency> <groupId>com.github.penggle</groupId> <artifactId>kaptcha</artifactId> <version>2.3.2</version></dependency>Kaptcha 相干配置配置kaptcha实例 @Beanpublic Producer captcha() { // 配置图形验证码的基本参数 Properties properties = new Properties(); // 图片宽度 properties.setProperty("kaptcha.image.width", "150"); // 图片长度 properties.setProperty("kaptcha.image.height", "50"); // 字符集 properties.setProperty("kaptcha.textproducer.char.string", "0123456789"); // 字符长度 properties.setProperty("kaptcha.textproducer.char.length", "4"); Config config = new Config(properties); // 应用默认的图形验证码实现,当然也能够自定义实现 DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); defaultKaptcha.setConfig(config); return defaultKaptcha;}配置kaptcha路由 @Controllerpublic class CaptchaController { @Autowired private Producer captchaProducer; @GetMapping("/captcha.jpg") public void getCaptcha(HttpServletRequest request, HttpServletResponse response) throws IOException { // 设置内容类型 response.setContentType("image/jpeg"); // 创立验证码文本 String capText = captchaProducer.createText(); // 将验证码文本设置到session request.getSession().setAttribute("captcha", capText); // 创立验证码图片 BufferedImage bi = captchaProducer.createImage(capText); // 获取响应输入流 ServletOutputStream out = response.getOutputStream(); // 将图片验证码数据写到响应输入流 ImageIO.write(bi, "jpg", out); // 推送并敞开响应输入流 try { out.flush(); } finally { out.close(); } }}在security config办法中对所有kaptcha申请放行 ...

November 2, 2020 · 2 min · jiezi

关于spring-security:springBoot整合spring-security实现权限管理单体应用版筑基初期

写在后面在后面的学习当中,咱们对spring security有了一个小小的意识,接下来咱们整合目前的支流框架springBoot,实现权限的治理。 在这之前,假设你曾经理解了基于资源的权限治理模型。数据库设计的表有 user 、role、user_role、permission、role_permission。 步骤:默认大家都曾经数据库曾经好,曾经有了下面提到的表。(文末提供sql脚本下载) 第一步:在pom.xml文件中引入相干jar包<?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 https://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.3.3.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>pers.lbf</groupId> <artifactId>springboot-spring-securioty-demo1</artifactId> <version>0.0.1-SNAPSHOT</version> <name>springboot-spring-security-demo1</name> <description>Demo project for Spring Boot</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.1.3</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> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </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>第二步:批改application.yml文件,增加数据库相干配置server: port: 8081spring: datasource: url: jdbc:mysql://127.0.0.1:3306/secutiry_authority?useSSL=false&serverTimezone=GMT username: root password: root1997 driver-class-name: com.mysql.cj.jdbc.Driver第三步:启动我的项目springboot曾经给咱们提供好了一个默认的username为“user”,其明码能够在控制台输入中失去。并且在springBoot的默认配置中,所有资源必须要通过认证后能力拜访 ...

October 14, 2020 · 4 min · jiezi

关于spring-security:Spring-Security学习笔记一

1 Spring Security1.1 简介Spring Security是一个弱小和高度可定制的认证和管制拜访框架,是基于Spring利用的事实上的平安规范,次要聚焦于为Java利用提供受权和认证性能,次要性能如下: 对认证和受权的全面和可扩大反对避免诸如Session Fixation、点击劫持、跨站点申请伪造攻打Servlet API集成Spring MVC可全集成1.2 已集成的认证技术HTTP BASIC authentication headers/HTTP Digest authentication headers/HTTP X.509 client certificate exchange:基于IETF RFC的规范LDAP:常见的跨平台身份验证形式Form-based authentication:用于简略的用户界面需要OpenID authentication:一种去中心化的身份认证形式Authentication based on pre-established request headers:一种用户身份验证以及受权的集中式平安根底计划Jasig Central Authentication Service:单点登录计划Transparent authentication context propagation for Remote Method Invocation and HttpInvoker:一个Spring近程调用协定Automatic "remember-me" authentication:容许在指定到期工夫前自行从新登录零碎Anonymous authentication:容许匿名用户应用特定身份平安拜访资源Run-as authentication:容许在一个会话中变换用户身份的机制Java Authentication and Authorization:JASS,Java验证和受权APIJava EE container authentication:容许零碎持续应用容器治理这种身份验证形式Kerberos:一种应用对称密钥机制,容许客户端和服务器互相确认身份的认证协定除此之外还引入了其余第三方包,比方JOSSO,另外如果都无奈满足需要,Spring Security也容许开发人员本人编写认证技术。 2 简略Demo上面来动手做一个简略的Demo去体验一下Spring Security。 2.1 新建工程 默认即可: 依赖抉择Spring Web和Spring Secutiry: 2.2 启动类批改启动类,使其作为Controller类: @SpringBootApplication@RestControllerpublic class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } @GetMapping("/") public String hello(){ return "Hello, Spring Security."; }}2.3 运行运行后能够看见控制台有一串主动生成的明码: ...

October 13, 2020 · 1 min · jiezi

关于spring-security:Spring-Security一Simple-Demo

Simple Demo该系列都是基于前后端拆散的形式,返回的数据都是应用的 JSON,以及应用了自定义的返回后果 starter:https://gitee.com/lin-mt/result-spring-boot。 源码地址:https://gitee.com/lin-mt/spring-boot-examples/tree/master/spring-security-data-permission-control新建一个 SpringBoot 我的项目,引入相干依赖<dependency> <artifactId>spring-boot-starter-data-jpa</artifactId> <groupId>org.springframework.boot</groupId></dependency><dependency> <artifactId>spring-boot-starter-security</artifactId> <groupId>org.springframework.boot</groupId></dependency><dependency> <artifactId>spring-boot-starter-web</artifactId> <groupId>org.springframework.boot</groupId></dependency><dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId></dependency><dependency> <artifactId>mysql-connector-java</artifactId> <groupId>mysql</groupId> <scope>runtime</scope></dependency><dependency> <groupId>com.gitee.lin-mt</groupId> <artifactId>result-spring-boot-starter</artifactId></dependency>自定义用户信息/** * 用户信息. * * @author <a href="mailto:lin-mt@outlook.com">lin-mt</a> */@Entity@Table(name = "sys_user")public class SysUser extends BaseEntity implements UserDetails, CredentialsContainer { private String username; @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) private String secretCode; private int accountNonExpired; private int accountNonLocked; private int credentialsNonExpired; private int enabled; @Transient private Collection<? extends GrantedAuthority> authorities; // setter and getter @Basic @Override @Column(name = "username") public String getUsername() { return username; } @Override @Transient @JsonIgnore public String getPassword() { return getSecretCode(); } @Override @Transient public boolean isAccountNonExpired() { return 0 == this.accountNonExpired; } @Override @Transient public boolean isAccountNonLocked() { return 0 == this.accountNonLocked; } @Override @Transient public boolean isCredentialsNonExpired() { return 0 == this.credentialsNonExpired; } @Override @Transient public boolean isEnabled() { return 1 == this.enabled; } @Override public void eraseCredentials() { this.secretCode = null; } @Override public String toString() { return "SysUser{" + "username='" + username + '\'' + ", gender='" + gender + '\'' + ", phoneNumber='" + phoneNumber + '\'' + ", emailAddress='" + emailAddress + '\'' + ", accountNonExpired=" + accountNonExpired + ", accountNonLocked=" + accountNonLocked + ", credentialsNonExpired=" + credentialsNonExpired + ", enabled=" + enabled + ", authorities=" + authorities + '}'; }}为什么要实现接口 org.springframework.security.core.userdetails.UserDetails 呢?首先,Spring Security 必定须要依据用户输出的某个条件(通常是用户名,也就是 username )获取该条件对应的用户信息,而后再依据登录人输出的信息以及对应的用户信息去验证是否可能登录零碎。那么 Spring Security 怎么能力从用户信息中获取验证所须要的数据呢,用户信息是咱们返回给 Spring Security 的,无论是从内存还是数据库获取,都是包装成一个实体。重点来了,如果这个实体实现了某个接口,那么就能够将该实体向上转型为该接口的实体(这是 Java 根底哈),这时候就能够间接调用实体中接口的办法获取实体的数据!而后就能够依据这些数据验证登录人能不能进入零碎了,所以 UserDetails 接口中咱们要实现的几个办法中,返回的数据就是 Spring Security 用来验证的数据。 ...

August 16, 2020 · 4 min · jiezi

关于spring-security:Spring-Security一Simple-Demo

Simple Demo该系列都是基于前后端拆散的形式,返回的数据都是应用的 JSON,以及应用了自定义的返回后果 starter:https://gitee.com/lin-mt/result-spring-boot。 源码地址:https://gitee.com/lin-mt/spring-boot-examples/tree/master/spring-security-data-permission-control新建一个 SpringBoot 我的项目,引入相干依赖<dependency> <artifactId>spring-boot-starter-data-jpa</artifactId> <groupId>org.springframework.boot</groupId></dependency><dependency> <artifactId>spring-boot-starter-security</artifactId> <groupId>org.springframework.boot</groupId></dependency><dependency> <artifactId>spring-boot-starter-web</artifactId> <groupId>org.springframework.boot</groupId></dependency><dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId></dependency><dependency> <artifactId>mysql-connector-java</artifactId> <groupId>mysql</groupId> <scope>runtime</scope></dependency><dependency> <groupId>com.gitee.lin-mt</groupId> <artifactId>result-spring-boot-starter</artifactId></dependency>自定义用户信息/** * 用户信息. * * @author <a href="mailto:lin-mt@outlook.com">lin-mt</a> */@Entity@Table(name = "sys_user")public class SysUser extends BaseEntity implements UserDetails, CredentialsContainer { private String username; @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) private String secretCode; private int accountNonExpired; private int accountNonLocked; private int credentialsNonExpired; private int enabled; @Transient private Collection<? extends GrantedAuthority> authorities; // setter and getter @Basic @Override @Column(name = "username") public String getUsername() { return username; } @Override @Transient @JsonIgnore public String getPassword() { return getSecretCode(); } @Override @Transient public boolean isAccountNonExpired() { return 0 == this.accountNonExpired; } @Override @Transient public boolean isAccountNonLocked() { return 0 == this.accountNonLocked; } @Override @Transient public boolean isCredentialsNonExpired() { return 0 == this.credentialsNonExpired; } @Override @Transient public boolean isEnabled() { return 1 == this.enabled; } @Override public void eraseCredentials() { this.secretCode = null; } @Override public String toString() { return "SysUser{" + "username='" + username + '\'' + ", gender='" + gender + '\'' + ", phoneNumber='" + phoneNumber + '\'' + ", emailAddress='" + emailAddress + '\'' + ", accountNonExpired=" + accountNonExpired + ", accountNonLocked=" + accountNonLocked + ", credentialsNonExpired=" + credentialsNonExpired + ", enabled=" + enabled + ", authorities=" + authorities + '}'; }}为什么要实现接口 org.springframework.security.core.userdetails.UserDetails 呢?首先,Spring Security 必定须要依据用户输出的某个条件(通常是用户名,也就是 username )获取该条件对应的用户信息,而后再依据登录人输出的信息以及对应的用户信息去验证是否可能登录零碎。那么 Spring Security 怎么能力从用户信息中获取验证所须要的数据呢,用户信息是咱们返回给 Spring Security 的,无论是从内存还是数据库获取,都是包装成一个实体。重点来了,如果这个实体实现了某个接口,那么就能够将该实体向上转型为该接口的实体(这是 Java 根底哈),这时候就能够间接调用实体中接口的办法获取实体的数据!而后就能够依据这些数据验证登录人能不能进入零碎了,所以 UserDetails 接口中咱们要实现的几个办法中,返回的数据就是 Spring Security 用来验证的数据。 ...

August 16, 2020 · 4 min · jiezi

Spring-Security-架构简介

一、技术概述1.1 Spring vs Spring Boot vs Spring Security1.1.1 Spring FrameworkSpring Framework 为开发 Java 应用程序提供了全面的基础架构支持。它包含了一些不错的功能,如 "依赖注入",以及一些现成的模块: Spring JDBCSpring MVCSpring SecuritySpring AOPSpring ORM这些模块可以大大减少应用程序的开发时间。例如,在 Java Web 开发的早期,我们需要编写大量样板代码以将记录插入数据源。但是,通过使用 Spring JDBC 模块的 JDBCTemplate,我们可以仅通过少量配置将其简化为几行代码。 1.1.2 Spring BootSpring Boot 是基于 Spring Framework,它为你的 Spring 应用程序提供了自动装配特性,它的设计目标是让你尽可能快的上手应用程序的开发。以下是 Spring Boot 所拥有的一些特性: 可以创建独立的 Spring 应用程序,并且基于 Maven 或 Gradle 插件,可以创建可执行的 JARs 和 WARs;内嵌 Tomcat 或 Jetty 等 Servlet 容器;提供自动配置的 "starter" 项目对象模型(POMS)以简化 Maven 配置;尽可能自动配置 Spring 容器;提供一些常见的功能、如监控、WEB容器,健康,安全等功能;绝对没有代码生成,也不需要 XML 配置。1.1.3 Spring SecuritySpring Security 是一个能够为基于 Spring 的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在 Spring 应用上下文中配置的 Bean,充分利用了 Spring IoC(Inversion of Control 控制反转),DI(Dependency Injection 依赖注入)和 AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。 ...

November 2, 2019 · 5 min · jiezi

Spring-Security-实战干货必须掌握的一些内置-Filter

1. 前言上一文我们使用 Spring Security 实现了各种登录聚合的场面。其中我们是通过在 UsernamePasswordAuthenticationFilter 之前一个自定义的过滤器实现的。我怎么知道自定义过滤器要加在 UsernamePasswordAuthenticationFilter 之前。我在这个系列开篇说了 Spring Security 权限控制的一个核心关键就是 过滤器链 ,这些过滤器如下图进行过滤传递,甚至比这个更复杂!这只是一个最小单元。 Spring Security 内置了一些过滤器,他们各有各的本事。如果你掌握了这些过滤器,很多实际开发中的需求和问题都很容易解决。今天我们来见识一下这些内置的过滤器。 2. 内置过滤器初始化在 Spring Security 初始化核心过滤器时 HttpSecurity 会通过将 Spring Security 内置的一些过滤器以 FilterComparator 提供的规则进行比较按照比较结果进行排序注册。 2.1 排序规则FilterComparator 维护了一个顺序的注册表 filterToOrder 。 FilterComparator() { Step order = new Step(INITIAL_ORDER, ORDER_STEP); put(ChannelProcessingFilter.class, order.next()); put(ConcurrentSessionFilter.class, order.next()); put(WebAsyncManagerIntegrationFilter.class, order.next()); put(SecurityContextPersistenceFilter.class, order.next()); put(HeaderWriterFilter.class, order.next()); put(CorsFilter.class, order.next()); put(CsrfFilter.class, order.next()); put(LogoutFilter.class, order.next()); filterToOrder.put( "org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter", order.next()); filterToOrder.put( "org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationRequestFilter", order.next()); put(X509AuthenticationFilter.class, order.next()); put(AbstractPreAuthenticatedProcessingFilter.class, order.next()); filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter", order.next()); filterToOrder.put( "org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter", order.next()); filterToOrder.put( "org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter", order.next()); put(UsernamePasswordAuthenticationFilter.class, order.next()); put(ConcurrentSessionFilter.class, order.next()); filterToOrder.put( "org.springframework.security.openid.OpenIDAuthenticationFilter", order.next()); put(DefaultLoginPageGeneratingFilter.class, order.next()); put(DefaultLogoutPageGeneratingFilter.class, order.next()); put(ConcurrentSessionFilter.class, order.next()); put(DigestAuthenticationFilter.class, order.next()); filterToOrder.put( "org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter", order.next()); put(BasicAuthenticationFilter.class, order.next()); put(RequestCacheAwareFilter.class, order.next()); put(SecurityContextHolderAwareRequestFilter.class, order.next()); put(JaasApiIntegrationFilter.class, order.next()); put(RememberMeAuthenticationFilter.class, order.next()); put(AnonymousAuthenticationFilter.class, order.next()); filterToOrder.put( "org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter", order.next()); put(SessionManagementFilter.class, order.next()); put(ExceptionTranslationFilter.class, order.next()); put(FilterSecurityInterceptor.class, order.next()); put(SwitchUserFilter.class, order.next()); }这些就是所有内置的过滤器。 他们是通过下面的方法获取自己的序号: ...

October 22, 2019 · 3 min · jiezi

Spring-security五完美权限管理系统授权过程分析

1. 权限管理相关概念 权限管理是一个几乎所有后台系统的都会涉及的一个重要组成部分,主要目的是对整个后台管理系统进行权限的控制。常见的基于角色的访问控制,其授权模型为“用户-角色-权限”,简明的说,一个用户拥有多个角色,一个角色拥有多个权限。其中, 用户: 不用多讲,大家也知道了;角色: 一个集合的概念,角色管理是确定角色具备哪些权限的一个过程 ;权限:1).页面权限,控制你可以看到哪个页面,看不到哪个页面; 2). 操作权限,控制你可以在页面上进行哪些操作(查询、删除、编辑等); 3).数据权限,是控制你可以看到哪些数据。 实质是: 权限(Permission) = 资源(Resource) + 操作(Privilege) 角色(Role) = 权限的集合(a set of low-level permissions) 用户(User) = 角色的集合(high-level roles) 权限管理过程: 鉴权管理,即权限判断逻辑,如菜单管理(普通业务人员登录系统后,是看不到【用户管理】菜单的)、功能权限管理(URL访问的管理)、行级权限管理等授权管理,即权限分配过程,如直接对用户授权,直接分配到用户的权限具有最优先级别、对用户所属岗位授权,用户所属岗位信息可以看作是一个分组,和角色的作用一样,但是每个用户只能关联一个岗位信息等。 在实际项目中用户数量多,逐一的为每个系统用户授权,这是极其繁琐的事,所以可以学习linux文件管理系统一样,设置group模式,一组有多个用户,可以为用户组授权相同的权限,简便多了。这样模式下: 每个用户的所有权限=用户个人的权限+用户组所用的权限 用户组、用户、与角色三者关系如下: 再结合权限管理的页面权限、操作权限,如菜单的访问、功能模块的操作、按钮的操作等等,可把功能操作与资源统一管理,即让它们直接与权限关联起来,关系图如下: 2. 授权过程分析### 2.1 授权访问权限工作流程: FilterSecurityInterceptor doFilter()->invoke() ->AbstractSecurityInterceptor beforeInvocation() ->SecurityMetadataSource 获取ConfigAttribute属性信息(从数据库或者其他数据源地方) getAttributes() ->AccessDecisionManager() 基于AccessDecisionVoter实现授权访问 Decide() ->AccessDecisionVoter 受AccessDecisionManager委托实现授权访问 vote()默认授权过程会使用这样的工作流程,接下来来分析各个组件的功能与源码。 ### 2.2 AbstractSecurityInterceptor分析 FilterSecurityInterceptor为授权拦截器, 在FilterSecurityInterceptor中有一个封装了过滤链、request以及response的FilterInvocation对象进行操作,在FilterSecurityInterceptor,主要由invoke()调用其父类AbstractSecurityInterceptor的方法。 invoke()分析: public void invoke(FilterInvocation fi) throws IOException, ServletException { ..... // 获取accessDecisionManager权限决策后结果状态、以及权限属性 InterceptorStatusToken token = super.beforeInvocation(fi); try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.finallyInvocation(token); } super.afterInvocation(token, null); }} AbstractSecurityInterceptor 的授权过滤器主要方法beforeInvocation(),afterInvocation()以及authenticateIfRequired(),其最主要的方法beforeInvocation() 分析如下: ...

October 14, 2019 · 4 min · jiezi

Spring-Security-实战干货用户信息UserDetails相关入门

1. 前言前一篇介绍了 Spring Security 入门的基础准备。从今天开始我们来一步步窥探它是如何工作的。我们又该如何驾驭它。请多多关注公众号: Felordcn 。本篇将通过 Spring Boot 2.x 来讲解 Spring Security 中的用户主体UserDetails。以及从中找点乐子。 2. Spring Boot 集成 Spring Security这个简直老生常谈了。不过为了照顾大多数还是说一下。集成 Spring Security 只需要引入其对应的 Starter 组件。Spring Security 不仅仅能保护Servlet Web 应用,也可以保护Reactive Web应用,本文我们讲前者。我们只需要在 Spring Security 项目引入以下依赖即可: <dependencies> <!-- actuator 指标监控 非必须 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!-- spring security starter 必须 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- spring mvc servlet web 必须 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- lombok 插件 非必须 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </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>3. UserDetailsServiceAutoConfiguration启动项目,访问Actuator端点http://localhost:8080/actuator会跳转到一个登录页面http://localhost:8080/login如下: ...

October 9, 2019 · 3 min · jiezi

Spring-security-一架构框架ComponentServiceFilter分析

想要深入spring security的authentication (身份验证)和access-control(访问权限控制)工作流程,必须清楚spring security的主要技术点包括关键接口、类以及抽象类如何协同工作进行authentication 和access-control的实现。 1.spring security 认证和授权流程常见认证和授权流程可以分成: A user is prompted to log in with a username and password (用户用账密码登录)The system (successfully) verifies that the password is correct for the username(校验密码正确性)The context information for that user is obtained (their list of roles and so on).(获取用户信息context,如权限)A security context is established for the user(为用户创建security context)The user proceeds, potentially to perform some operation which is potentially protected by an access control mechanism which checks the required permissions for the operation against the current security context information.(访问权限控制,是否具有访问权限)1.1 spring security 认证上述前三点为spring security认证验证环节: ...

October 7, 2019 · 4 min · jiezi

Spring-里那么多种-CORS-的配置方式到底有什么区别

作为一个后端开发,我们经常遇到的一个问题就是需要配置 CORS,好让我们的前端能够访问到我们的 API,并且不让其他人访问。而在 Spring 中,我们见过很多种 CORS 的配置,很多资料都只是告诉我们可以这样配置、可以那样配置,但是这些配置有什么区别? CORS 是什么首先我们要明确,CORS 是什么,以及规范是如何要求的。这里只是梳理一下流程,具体的规范请看 这里。 CORS 全称是 Cross-Origin Resource Sharing,直译过来就是跨域资源共享。要理解这个概念就需要知道域、资源和同源策略这三个概念。 域,指的是一个站点,由 protocal、host 和 port 三部分组成,其中 host 可以是域名,也可以是 ip ;port 如果没有指明,则是使用 protocal 的默认端口资源,是指一个 URL 对应的内容,可以是一张图片、一种字体、一段 HTML 代码、一份 JSON 数据等等任何形式的任何内容同源策略,指的是为了防止 XSS,浏览器、客户端应该仅请求与当前页面来自同一个域的资源,请求其他域的资源需要通过验证。了解了这三个概念,我们就能理解为什么有 CORS 规范了:从站点 A 请求站点 B 的资源的时候,由于浏览器的同源策略的影响,这样的跨域请求将被禁止发送;为了让跨域请求能够正常发送,我们需要一套机制在不破坏同源策略的安全性的情况下、允许跨域请求正常发送,这样的机制就是 CORS。 预检请求在 CORS 中,定义了一种预检请求,即 preflight request,当实际请求不是一个 简单请求 时,会发起一次预检请求。预检请求是针对实际请求的 URL 发起一次 OPTIONS 请求,并带上下面三个 headers : Origin:值为当前页面所在的域,用于告诉服务器当前请求的域。如果没有这个 header,服务器将不会进行 CORS 验证。Access-Control-Request-Method:值为实际请求将会使用的方法Access-Control-Request-Headers:值为实际请求将会使用的 header 集合如果服务器端 CORS 验证失败,则会返回客户端错误,即 4xx 的状态码。 否则,将会请求成功,返回 200 的状态码,并带上下面这些 headers: ...

June 15, 2019 · 6 min · jiezi

Grails3-Spring-Secuirty自定义加密方式

Grails3 Spring Secuirty自定义加密方式应用场景公司老项目使用grails2.0+版本,他的加密方式为encodeAsSHA256,数据是通过导入实现,要兼容以前数据加密方式,使以前使用老项目的用户也能用原先的密码登录。首先,我做了一下测试def test() { map.password1 = "123456".encodeAsSHA256() map.password2 = springSecurityService.encodePassword("123456") render map as JSON}结果发现他们是两种不同的加密方式接下来只能通过重写父类方法实现统一加密第一步:自定义一个类,我习惯在src/main/goovy下创建CustomPasswordEncoder.groovy类,也可以创建java类package com.encoderimport org.springframework.security.authentication.encoding.MessageDigestPasswordEncoderimport org.springframework.security.authentication.encoding.PasswordEncoderUtilsimport org.springframework.security.crypto.codec.Heximport org.springframework.util.Assertimport java.security.MessageDigest/** * 自定义加密覆盖默认加密方式 * 项目spring-security版本为3.1.0,本可以重新BaseDigestPasswordEncoder类 * 但是本人看BaseDigestPasswordEncoder类被标记为删除了,所以通过重写MessageDigestPasswordEncoder类方法实现 */class CustomPasswordEncoder extends MessageDigestPasswordEncoder { // 默认为MD5 private String algorithm = "MD5"; // 加密次数(提高安全) private int iterations = 1; CustomPasswordEncoder() { // 当前类默认构造器,因为父类没有空构造器,所以这里必须调用父类有参构造,这里传入参数必须是父类存在的加密规则,否则报错 super("SHA-256") } CustomPasswordEncoder(String algorithm) { super(algorithm, false); this.algorithm = algorithm } CustomPasswordEncoder(String algorithm, boolean encodeHashAsBase64) throws IllegalArgumentException { super() setEncodeHashAsBase64(encodeHashAsBase64); this.algorithm = algorithm; getMessageDigest(); } @Override String encodePassword(String rawPass, Object salt) { String saltedPass = this.mergePasswordAndSalt(rawPass, salt, false) MessageDigest messageDigest = this.getMessageDigest() byte[] digest = messageDigest.digest(saltedPass.getBytes("UTF-8")) for (int i = 1; i < iterations; i++) { digest = messageDigest.digest(digest); } // 先判断是否启用base64 if (this.getEncodeHashAsBase64()) { return new String(Base64.encodeAsBase64(digest)) // 判断是否为自定义的SHA-256-1(框架自定加密方式,非spring security框架,这里指的是grails自带的加密) } else if ("SHA-256-1".equalsIgnoreCase(algorithm)) { return rawPass.encodeAsSHA256() } else { // 使用用户配置的其他加密方式 return new String(Hex.encode(digest)) } } @Override boolean isPasswordValid(String encPass, String rawPass, Object salt) { String pass1 = "" + encPass String pass2 = encodePassword(rawPass, salt) return PasswordEncoderUtils.equals(pass1, pass2) } String getAlgorithm() { return algorithm; } void setIterations(int iterations) { Assert.isTrue(iterations > 0, "Iterations value must be greater than zero"); this.iterations = iterations; }}第二步:在grails-app/conf/spring/resources.groovy中注册一下beanbeans = { // 自定义密码 passwordEncoder(com.encoder.CustomPasswordEncoder) { encodeHashAsBase64 = false // 若为true,则以base64方式加密 }}第三步:在grails-app/conf/application.groovy中添加配置// 原框架加密方式,有SHA-256、bcrypt、MD5、pbkdf2,默认为bcrypt// 自定义加密默认为MD5,这里SHA-256-1为自定义加密,还可以用SHA-256等grails.plugin.springsecurity.password.algoritham = 'SHA-256-1'到这里配置就已经完成了,启动测试注意:项目中用到domain.save(failOnError: true)的有时需要修改为domain.save(flush: true),我测试密码修改时,failOnError: true没有修改成功。参考 ...

June 14, 2019 · 1 min · jiezi

spring security集成cas

spring security集成cas源码地址在文章末尾,转载请注明出处,谢谢。0.配置本地ssl连接操作记录如下:=====================1.创建证书文件thekeystore ,并导出为thekeystore.crtcd C:\Users\23570\keystoreC:\Users\23570\keystore>keytool -genkey -keyalg RSA -alias thekeystore -keystore thekeystore输入密钥库口令:changeit再次输入新口令:changeit您的名字与姓氏是什么? [Unknown]: localhost您的组织单位名称是什么? [Unknown]: localhost您的组织名称是什么? [Unknown]:您所在的城市或区域名称是什么? [Unknown]:您所在的省/市/自治区名称是什么? [Unknown]:该单位的双字母国家/地区代码是什么? [Unknown]:CN=localhost, OU=localhost, O=Unknown, L=Unknown, ST=Unknown, C=Unknown是否正确? [否]: y输入 <thekeystore> 的密钥口令 (如果和密钥库口令相同, 按回车):Warning:JKS 密钥库使用专用格式。建议使用 “keytool -importkeystore -srckeystore thekeystore -destkeystore thekeystore -deststoretype pkcs12” 迁移到行业标准格式 PKCS12。C:\Users\23570\keystore>keytool -export -alias thekeystore -file thekeystore.crt -keystore thekeystore输入密钥库口令:存储在文件 <thekeystore.crt> 中的证书Warning:JKS 密钥库使用专用格式。建议使用 “keytool -importkeystore -srckeystore thekeystore -destkeystore thekeystore -deststoretype pkcs12” 迁移到行业标准格式 PKCS12。======================2.把证书文件导入到本地证书库中,注意切换JRE相应目录切换为【管理员身份】运行以下命令:C:\Users\23570\keystore>keytool -import -alias thekeystore -storepass changeit -file thekeystore.crt -keystore “C:\Program Files\Java\jdk1.8.0_191\jre\lib\security\cacerts"所有者: CN=localhost, OU=localhost, O=Unknown, L=Unknown, ST=Unknown, C=Unknown发布者: CN=localhost, OU=localhost, O=Unknown, L=Unknown, ST=Unknown, C=Unknown序列号: 657eb9ce有效期为 Fri Mar 29 11:50:08 CST 2019 至 Thu Jun 27 11:50:08 CST 2019证书指纹: MD5: 8D:3C:78:E9:8A:44:77:3F:C2:8B:20:95:C7:6C:91:8F SHA1: 69:F3:46:C4:03:95:E1:D0:E6:9D:8B:72:F4:EB:ED:13:8B:9A:6A:38 SHA256: 79:D1:F8:B2:1B:E3:AF:D4:4F:35:CB:6B:C8:84:3F:85:21:13:0F:96:4A:B5:E5:4C:47:11:44:21:8F:F3:2D:83签名算法名称: SHA256withRSA主体公共密钥算法: 2048 位 RSA 密钥版本: 3扩展:#1: ObjectId: 2.5.29.14 Criticality=falseSubjectKeyIdentifier [KeyIdentifier [0000: B0 38 1D 00 56 65 EE 98 7C 35 58 04 B5 2E C0 A0 .8..Ve…5X…..0010: D5 C2 C5 B5 ….]]是否信任此证书? [否]: y证书已添加到密钥库中=========================3.配置tomcat/conf/server.xml中的ssl连接<Connector port=“8443” protocol=“org.apache.coyote.http11.Http11NioProtocol” maxThreads=“200” SSLEnabled=“true” scheme=“https” secure=“true” clientAuth=“false” sslProtocol=“TLS” keystoreFile=“C:\Users\23570\keystore\thekeystore” keystorePass=“changeit”/> ==========================4.其他命令参考删除JRE中指定别名的证书keytool -delete -alias cas.server.com -keystore “C:\Program Files\Java\jdk1.8.0_191\jre\lib\security\cacerts"查看JRE中指定别名的证书keytool -list -v -keystore “C:\Program Files\Java\jdk1.8.0_191\jre\lib\security\cacerts” -alias cas.server.com 1.cas服务搭建git clone –branch 5.3 https://github.com/apereo/cas-overlay-template.git cas-server注意:这里选用cas server 5.3版本,使用maven构建1.使用数据库账号密码登录cas导入依赖<dependency> <groupId>org.apereo.cas</groupId> <artifactId>cas-server-support-jdbc</artifactId> <version>${cas.version}</version></dependency><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version></dependency>配置查询#这里是配置用户表单登录时用户名字段为usernamecas.authn.jdbc.query[0].sql=select password from oauth_account left join oauth_user on oauth_account.user_id=oauth_user.user_id where oauth_user.username=?;cas.authn.jdbc.query[0].fieldPassword=passwordcas.authn.jdbc.query[0].fieldExpired=expiredcas.authn.jdbc.query[0].fieldDisabled=disabledcas.authn.jdbc.query[0].dialect=org.hibernate.dialect.MySQLDialectcas.authn.jdbc.query[0].driverClass=com.mysql.jdbc.Drivercas.authn.jdbc.query[0].url=jdbc:mysql://127.0.0.1:3306/srm-aurora2?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=falsecas.authn.jdbc.query[0].user=rootcas.authn.jdbc.query[0].password=root#默认不加密#cas.authn.jdbc.query[0].passwordEncoder.type=NONE#默认加密策略,通过encodingAlgorithm来指定算法,默认NONE不加密cas.authn.jdbc.query[0].passwordEncoder.type=DEFAULTcas.authn.jdbc.query[0].passwordEncoder.characterEncoding=UTF-8cas.authn.jdbc.query[0].passwordEncoder.encodingAlgorithm=MD5#配置用户表单登录时用户名字段为phonecas.authn.jdbc.query[1].sql=select password from oauth_account left join oauth_user on oauth_account.user_id=oauth_user.user_id where oauth_user.phone=?;cas.authn.jdbc.query[1].fieldPassword=passwordcas.authn.jdbc.query[1].fieldExpired=expiredcas.authn.jdbc.query[1].fieldDisabled=disabledcas.authn.jdbc.query[1].dialect=org.hibernate.dialect.MySQLDialectcas.authn.jdbc.query[1].driverClass=com.mysql.jdbc.Drivercas.authn.jdbc.query[1].url=jdbc:mysql://127.0.0.1:3306/srm-aurora2?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=falsecas.authn.jdbc.query[1].user=rootcas.authn.jdbc.query[1].password=root#默认不加密#cas.authn.jdbc.query[0].passwordEncoder.type=NONE#默认加密策略,通过encodingAlgorithm来指定算法,默认NONE不加密cas.authn.jdbc.query[1].passwordEncoder.type=DEFAULTcas.authn.jdbc.query[1].passwordEncoder.characterEncoding=UTF-8cas.authn.jdbc.query[1].passwordEncoder.encodingAlgorithm=MD5数据库脚本/* Navicat Premium Data Transfer Source Server : localhost Source Server Type : MySQL Source Server Version : 50722 Source Host : localhost:3306 Source Schema : srm-aurora2 Target Server Type : MySQL Target Server Version : 50722 File Encoding : 65001 Date: 19/04/2019 14:40:52*/SET NAMES utf8mb4;SET FOREIGN_KEY_CHECKS = 0;– —————————— Table structure for oauth_account– —————————-DROP TABLE IF EXISTS oauth_account;CREATE TABLE oauth_account ( account_id int(255) NOT NULL AUTO_INCREMENT, tenant_id int(255) NULL DEFAULT NULL, user_id int(255) NULL DEFAULT NULL, password varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (account_id) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;– —————————— Records of oauth_account– —————————-INSERT INTO oauth_account VALUES (1, 1, 1, ’e10adc3949ba59abbe56e057f20f883e’);INSERT INTO oauth_account VALUES (2, 2, 2, ’e10adc3949ba59abbe56e057f20f883e’);– —————————— Table structure for oauth_cas_info– —————————-DROP TABLE IF EXISTS oauth_cas_info;CREATE TABLE oauth_cas_info ( cas_id int(255) NOT NULL, tenant_id int(255) NULL DEFAULT NULL, cas_server varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, cas_server_login varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, cas_server_logout varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, cas_service varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, cas_service_logout varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (cas_id) USING BTREE) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;– —————————— Records of oauth_cas_info– —————————-INSERT INTO oauth_cas_info VALUES (1, 2, ‘https://localhost:8443/cas’, ‘https://localhost:8443/cas/login?service=http%3A%2F%2Flocalhost%3A8083%2Flogin%2Fcas’, ‘https://localhost:8443/cas/logout’, ‘http://localhost:8083/login/cas’, ‘https://localhost:8443/cas/logout?service=http://localhost:8083/logout/success’);INSERT INTO oauth_cas_info VALUES (2, 3, ‘https://localhost:9443/sso’, ‘https://localhost:9443/sso/login?service=http%3A%2F%2Flocalhost%3A8083%2Flogin%2Fcas’, ‘https://localhost:9443/sso/logout’, ‘http://localhost:8083/login/cas’, ‘https://localhost:9443/sso/logout?service=http://localhost:8083/logout/success’);– —————————— Table structure for oauth_tenant– —————————-DROP TABLE IF EXISTS oauth_tenant;CREATE TABLE oauth_tenant ( tenant_id int(255) NOT NULL AUTO_INCREMENT, domain varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, name varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, login_provider varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, login_type varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (tenant_id) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;– —————————— Records of oauth_tenant– —————————-INSERT INTO oauth_tenant VALUES (1, ‘http://localhost:8084/’, ‘a租户’, ‘oauth’, ‘form’);INSERT INTO oauth_tenant VALUES (2, ‘http://localhost:8085/’, ‘b租户’, ‘cas’, ‘wechat’);INSERT INTO oauth_tenant VALUES (3, ‘http://localhost:8086/’, ‘c租户’, ‘cas’, ‘form’);– —————————— Table structure for oauth_user– —————————-DROP TABLE IF EXISTS oauth_user;CREATE TABLE oauth_user ( user_id int(255) NOT NULL AUTO_INCREMENT, username varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, phone varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, email varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (user_id) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;– —————————— Records of oauth_user– —————————-INSERT INTO oauth_user VALUES (1, ‘22304’, ‘15797656200’, ‘donglin.ling@hand-china.com’);INSERT INTO oauth_user VALUES (2, ‘admin’, ‘15797656201’, ’ericling666@gmail.com’);SET FOREIGN_KEY_CHECKS = 1;发布cas server,访问:https://localhost:8443/cas/login测试账号和密码,admin:1234562.CAS客户端服务注册这里演示通过json文件注册服务,实际项目中,可以配置成从数据库中注册添加json支持依赖<!–json服务注册–><dependency> <groupId>org.apereo.cas</groupId> <artifactId>cas-server-support-json-service-registry</artifactId> <version>${cas.version}</version></dependency>添加json服务注册文件{ “@class” : “org.apereo.cas.services.RegexRegisteredService”, “serviceId” : “^(https|http|imaps)://.”, “name” : “HTTPS and HTTP and IMAPS”, “id” : 10000001, “description” : “This service definition authorizes all application urls that support HTTPS and HTTP and IMAPS protocols.”, “evaluationOrder” : 10000, “attributeReleasePolicy”: { “@class”: “org.apereo.cas.services.ReturnAllAttributeReleasePolicy” }, “proxyPolicy”: { “@class”: “org.apereo.cas.services.RegexMatchingRegisteredServiceProxyPolicy”, “pattern”: “^(https|http)?://.” }}注意文件目录和文件名格式:目录:resources/services/{xxx}-{id}.jsonxxx表示可以随意配置,后面-{id},这里的id需要和文件中的id一致。作为演示,这个json注册文件,没有限制域名,也就是说所有的服务都可以注册成功。开启json服务注册### 开启json服务注册#cas.serviceRegistry.initFromJson=true以上就是配置json服务注册的过程。3.其它常用配置### 登出后允许跳转到指定页面#cas.logout.followServiceRedirects=true# 设置service ticket的行为# cas.ticket.st.maxLength=20# cas.ticket.st.numberOfUses=1cas.ticket.st.timeToKillInSeconds=120# 设置proxy ticket的行为cas.ticket.pt.timeToKillInSeconds=120# cas.ticket.pt.numberOfUses=1配置说明:配置cas服务登出时,是否跳转到各个子服务的登出页面,默认false【即默认情况下,子服务点击登出,用户统一跳转到cas的登出页面】,子服务登出时访问cas登出端点,并带上service。示例:https://localhost:8443/cas/logout?service=http://localhost:8083/logout/success这样配置,cas注销session之后,会重定向到service。这个字段可以配置,默认是service。配置如下:cas.logout.redirectParameter=service配置service ticket的失效时间,我这里配置这个选项,是为了方便后面debug调试,实际生产中,不必配置这个选项。更多常用配置项,请查看官网链接:https://apereo.github.io/cas/…2.spring security和cas集成1.依赖和其他配置核心依赖<!–security-cas集成–><dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-cas</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency>application.yml配置# 我这里是为了方便调试logging.level.org.springframework.security: debuglogging.level.web: debug2.配置登录端点spring security开启表单登陆@Override protected void configure(HttpSecurity http) throws Exception { http.formLogin().loginPage("/login”); }这个配置,会开启用户表单登录,并且配置登录端点为/login配置登录端点响应逻辑@Controllerpublic class LoginEndpointConfig { @Autowired private TenantService tenantService; @Autowired private CasInfoService casInfoService; @GetMapping("/login”) public String loginJump(HttpSession session) { final String SAVED_REQUEST = “SPRING_SECURITY_SAVED_REQUEST”; Object attribute = session.getAttribute(SAVED_REQUEST); if (attribute == null) { //默认跳转到登陆页面 return “login”; } if (attribute instanceof DefaultSavedRequest) { DefaultSavedRequest savedRequest = (DefaultSavedRequest) attribute; List<String> referer = savedRequest.getHeaderValues(“referer”); if (referer.size() == 1) { //有referer请求头 String domain = referer.get(0); Tenant tenant = tenantService.selectByDomain(domain); if (tenant == null) { return “login”; } else { String loginProvider = tenant.getLoginProvider(); switch (loginProvider) { case “cas”: //获取cas地址 CasInfo casInfoByTenantId = casInfoService.getCasInfoByTenantId(tenant.getTenantId()); String casServerLogin = casInfoByTenantId.getCasServerLogin(); session.setAttribute(“casInfoByTenantId”,casInfoByTenantId); return “redirect:” + casServerLogin; case “oauth”: return “login”; default: return “login”; } } } else { return “login”; } } return “login”; }}我这里的登陆逻辑实现了:用户从第三方网站【平台的租户】跳转到这个网站时,根据跳转过来的请求头【referer】获取这个租户的域名,再从数据库中查找这个域名对应的租户信息和登录逻辑。这里的租户信息有一个关键字段是:loginProvider,有两种情况cas,oauthcas:租户有自己的cas单点登录系统,平台需要和租户的cas集成oauth:租户没有cas,使用平台统一的表单登陆具体的登录流程分析,在最后详细介绍,这里不过多讲解。3.配置CAS的ticket校验以及登录响应自定义AuthenticationFilter因为我的需求是,每个租户有自己的cas系统,所以每个cas地址不一样,不可能使用官方的CasAuthenticationFilter 。具体原因是,官方的CasAuthenticationFilter在应用程序启动时,资源匹配器就已经初始化好了,它只会对特定的cas地址发送ticket校验请求。而要做到可配置,就只能自己实现这个逻辑,并且可配置的对相应cas server地址发出ticket校验请求。public class CustomCasAuthenticationFilter extends AbstractAuthenticationProcessingFilter { private final static String endpoint = “/login/cas”; private UserDetailsService userDetailsService; public CustomCasAuthenticationFilter(String defaultFilterProcessesUrl, UserDetailsService userDetailsService) { super(defaultFilterProcessesUrl); this.userDetailsService = userDetailsService; } private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler(); private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler(); public CustomCasAuthenticationFilter() { super(new AntPathRequestMatcher(endpoint)); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; if (!requiresAuthentication(req, res)) { chain.doFilter(request, response); return; } String ticket = obtainArtifact(req); //开始校验ticket try { CasInfo casInfo = (CasInfo) req.getSession().getAttribute(“casInfoByTenantId”); if (StringUtils.hasText(casInfo.getCasServer())) { //获取当前项目地址 String service; int port = request.getServerPort(); if (port != 80) { service = request.getScheme() + “://” + request.getServerName() + “:” + request.getServerPort() + endpoint; } else { service = request.getScheme() + “://” + request.getServerName() + endpoint; } //开始校验ticket Assertion validateResult = getTicketValidator(casInfo.getCasServer()).validate(ticket, service); //根据校验结果,获取用户详细信息 UserDetails userDetails = null; try { userDetails = userDetailsService.loadUserByUsername(validateResult.getPrincipal().getName()); if (this.logger.isDebugEnabled()) { logger.debug(“userDetailsServiceImpl is loading username:"+validateResult.getPrincipal().getName()); } } catch (UsernameNotFoundException e) { unsuccessfulAuthentication(req, res, e); } //手动封装authentication对象 assert userDetails != null; UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(validateResult.getPrincipal(), ticket, userDetails.getAuthorities()); authentication.setDetails(userDetails); successfulAuthentication(req,res,chain,authentication); } else { unsuccessfulAuthentication(req, res, new BadCredentialsException(“bad credential:ticket校验失败”)); } } catch (TicketValidationException e) { //ticket校验失败 unsuccessfulAuthentication(req, res, new BadCredentialsException(e.getMessage())); }// chain.doFilter(request, response); } /** / public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { return null; } /* * 从HttpServletRequest请求中获取ticket / private String obtainArtifact(HttpServletRequest request) { String artifactParameter = “ticket”; return request.getParameter(artifactParameter); } /* * 获取Cas30ServiceTicketValidator,暂时没有实现代理凭据 / private TicketValidator getTicketValidator(String casServerUrlPrefix) { return new Cas30ServiceTicketValidator(casServerUrlPrefix); } protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { if (this.logger.isDebugEnabled()) { this.logger.debug(“Authentication success. Updating SecurityContextHolder to contain: " + authResult); } SecurityContextHolder.getContext().setAuthentication(authResult); if (this.eventPublisher != null) { this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass())); } this.successHandler.onAuthenticationSuccess(request, response, authResult); } protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { SecurityContextHolder.clearContext(); if (this.logger.isDebugEnabled()) { this.logger.debug(“Authentication request failed: " + failed.toString(), failed); this.logger.debug(“Updated SecurityContextHolder to contain null Authentication”); this.logger.debug(“Delegating to authentication failure handler " + this.failureHandler); } this.failureHandler.onAuthenticationFailure(request, response, failed); }}2. 把自定义的CustomCasAuthenticationFilter添加到spring security的过滤器链中@Qualifier(“userDetailsServiceImpl”) @Autowired private UserDetailsService userDetailsService;private final static String endpoint = “/login/cas”;@Override protected void configure(HttpSecurity http) throws Exception { http.addFilterAt(new CustomCasAuthenticationFilter(endpoint, userDetailsService), UsernamePasswordAuthenticationFilter.class);}### 4.配置单点登出1. 自定义实现LogoutFilterpublic class CustomLogoutFilter extends GenericFilterBean { private RequestMatcher logoutRequestMatcher; private SimpleUrlLogoutSuccessHandler urlLogoutSuccessHandler; private LogoutHandler logoutHandler = new SecurityContextLogoutHandler(); //获取casInfo信息,依此来判断当前认证用户的cas地址 private CasInfoService casInfoService; public CustomLogoutFilter(String filterProcessesUrl, String logoutSuccessUrl,CasInfoService casInfoService) { this.logoutRequestMatcher = new AntPathRequestMatcher(filterProcessesUrl); this.urlLogoutSuccessHandler=new SimpleUrlLogoutSuccessHandler(); this.urlLogoutSuccessHandler.setDefaultTargetUrl(logoutSuccessUrl); this.casInfoService = casInfoService; } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; if (requiresLogout(request, response)) { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (logger.isDebugEnabled()) { logger.debug(“Logging out user ‘” + auth + “’ and transferring to logout destination”); } //本地登出 logoutHandler.logout(request,response,auth); if (auth == null) { urlLogoutSuccessHandler.onLogoutSuccess(request,response, null); }else{ //判断是否通过cas认证,获取cas信息 Object details = auth.getDetails(); if (details == null) { urlLogoutSuccessHandler.onLogoutSuccess(request,response,auth); } if (details instanceof UserDetails) { Integer tenantId = ((UserDetailsVO) details).getTenant().getTenantId(); CasInfo casInfoByTenantId = casInfoService.getCasInfoByTenantId(tenantId); response.sendRedirect(casInfoByTenantId.getCasServiceLogout()); }else{ urlLogoutSuccessHandler.onLogoutSuccess(request,response,auth); } } return; } filterChain.doFilter(request, response); } /* * 当前请求是否为登出请求 */ private boolean requiresLogout(HttpServletRequest request, HttpServletResponse response) { return logoutRequestMatcher.matches(request); }}2. 把CustomLogoutFilter添加到spring security的过滤器链中@Override protected void configure(HttpSecurity http) throws Exception { http.addFilterAt(new CustomLogoutFilter("/logout”, “/logout/success”, casInfoService), LogoutFilter.class);}### 5.流程分析#### 1.表单登陆流程分析目前有5个服务cas server,tenant-a,tenant-b,tenant-c,a2-oauth租户a,b,c就是一个超链接而已,为了模拟三个租户的域名,所以弄了三个租户。这三个域名分别是:&lt;http://localhost:8084/&gt; , &lt;http://localhost:8085/&gt; , &lt;http://localhost:8086/&gt;数据库中,对这3个租户的配置如下:其中b和c租户是配置了cas登录的。cas server发布了两个,都开了SSL链接,分别是:https://localhost:8443/cas ,https://localhost:9443/sso我们先测试表单登录。启动租户a,访问链接http://localhost:8084 ,这个页面只有一个超链接,点击超链接,访问http://localhost:8083/oauth/authorize?client_id=youku&amp;response_type=token&amp;redirect_uri=http://localhost:8081/youku/qq/redirect查看日志://前面经过spring security的一堆过滤器链,都没有匹配到FrameworkEndpointHandlerMapping : Mapped to public org.springframework.web.servlet.ModelAndView org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint.authorize(java.util.Map<java.lang.String, java.lang.Object>,java.util.Map<java.lang.String, java.lang.String>,org.springframework.web.bind.support.SessionStatus,java.security.Principal)//用户未认证,无法授权,抛出异常,ExceptionTranslationFilter对异常处理,跳转到配置的authentication //entry point,这里的authentication entry point,就是我之前配置的/login端点2019-04-19 16:01:14.608 DEBUG 21568 — [nio-8083-exec-1] o.s.web.servlet.DispatcherServlet : Failed to complete request: org.springframework.security.authentication.InsufficientAuthenticationException: User must be authenticated with Spring Security before authorization can be completed.2019-04-19 16:01:14.611 DEBUG 21568 — [nio-8083-exec-1] o.s.s.w.a.ExceptionTranslationFilter : Authentication exception occurred; redirecting to authentication entry pointorg.springframework.security.authentication.InsufficientAuthenticationException: User must be authenticated with Spring Security before authorization can be completed.可以看到,已经进入到了controller里面。final String SAVED_REQUEST = “SPRING_SECURITY_SAVED_REQUEST”; Object attribute = session.getAttribute(SAVED_REQUEST);这段代码的作用是为了拿到,之前发起的请求。那么这个请求是什么时候被保存的呢?我们知道抛出异常之后,ExceptionTranslationFilter对异常进行处理,检测到用户没有登录,所以才跳转到authentication entry point,所以,猜想应该是这里保存了最开始的请求信息。以下是ExceptionTranslationFilter的核心代码:public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {this.handleSpringSecurityException(request, response, chain, (RuntimeException)ase);}private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, RuntimeException exception) throws IOException, ServletException { if (exception instanceof AuthenticationException) { this.logger.debug(“Authentication exception occurred; redirecting to authentication entry point”, exception); this.sendStartAuthentication(request, response, chain, (AuthenticationException)exception); } else if (exception instanceof AccessDeniedException) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (!this.authenticationTrustResolver.isAnonymous(authentication) && !this.authenticationTrustResolver.isRememberMe(authentication)) { this.logger.debug(“Access is denied (user is not anonymous); delegating to AccessDeniedHandler”, exception); this.accessDeniedHandler.handle(request, response, (AccessDeniedException)exception); } else { this.logger.debug(“Access is denied (user is " + (this.authenticationTrustResolver.isAnonymous(authentication) ? “anonymous” : “not fully authenticated”) + “); redirecting to authentication entry point”, exception); this.sendStartAuthentication(request, response, chain, new InsufficientAuthenticationException(this.messages.getMessage(“ExceptionTranslationFilter.insufficientAuthentication”, “Full authentication is required to access this resource”))); } }}这里对异常的处理,其实,核心就只有两个方法:1. this.accessDeniedHandler.handle(request, response, (AccessDeniedException)exception); ,这种情况下,用户已经登陆了,但是权限不够,所以交给accessDeniedHandler进行处理,一般来讲,如果没有进行特殊的配置,会返回一个403错误和异常信息【不再跳转到authentication entry point,因为用户已经登陆了】,这里不深究。2. this.sendStartAuthentication(request, response, chain, (AuthenticationException)exception); ,这个方法核心代码如下:protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AuthenticationException reason) throws ServletException, IOException { SecurityContextHolder.getContext().setAuthentication((Authentication)null); //就是在这里保存的这次请求的所有信息,包括请求头,请求路径,参数,cookie等详细信息。所以,后面跳转到/login端点时,我在controller里面可以拿出来。 this.requestCache.saveRequest(request, response); this.logger.debug(“Calling Authentication entry point.”); //这里就是发起用户认证了,根据我的配置,它就会跳转到/login this.authenticationEntryPoint.commence(request, response, reason); }再回到前面的controller登录逻辑,往下走:@GetMapping("/login”)public String loginJump(HttpSession session) {final String SAVED_REQUEST = “SPRING_SECURITY_SAVED_REQUEST”;Object attribute = session.getAttribute(SAVED_REQUEST);// 默认情况下,用户直接访问/login时,没有SAVED_REQUESTif (attribute == null) { //默认跳转到登陆页面 return “login”;}if (attribute instanceof DefaultSavedRequest) { DefaultSavedRequest savedRequest = (DefaultSavedRequest) attribute; List<String> referer = savedRequest.getHeaderValues(“referer”); if (referer.size() == 1) { //有referer请求头 String domain = referer.get(0); //获取到数据库中配置的租户信息 Tenant tenant = tenantService.selectByDomain(domain); if (tenant == null) { return “login”; } else { String loginProvider = tenant.getLoginProvider(); switch (loginProvider) { case “cas”: //获取cas地址 CasInfo casInfoByTenantId = casInfoService.getCasInfoByTenantId(tenant.getTenantId()); String casServerLogin = casInfoByTenantId.getCasServerLogin(); session.setAttribute(“casInfoByTenantId”,casInfoByTenantId); return “redirect:” + casServerLogin; case “oauth”: //因为我在数据库中配置的是oauth,所以,最后响应login视图 return “login”; default: return “login”; } } } else { return “login”; }}return “login”;}用户跳转到登陆页面输入用户名密码,点击登陆,进入UsernamePasswordAuthenticationFilter ,开始尝试认证用户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);}最终会调用AuthenticationManager接口的authenticate方法,而AuthenticationManager委托一堆的AuthenticationProvider来进行认证。后面的流程,不再赘述,不在本篇文章的讨论范畴。用户认证成功后,调用successfulAuthentication(request, response, chain, authResult); 其实,这个方法里面核心代码就是successHandler.onAuthenticationSuccess(request, response, authResult);AuthenticationSuccessHandler有很多实现类,我们也可以自定义实现AuthenticationSuccessHandler。最常用的实现是,SavedRequestAwareAuthenticationSuccessHandler ,看一下它里面的核心代码:@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { SavedRequest savedRequest = requestCache.getRequest(request, response); if (savedRequest == null) { super.onAuthenticationSuccess(request, response, authentication); return; } String targetUrlParameter = getTargetUrlParameter(); if (isAlwaysUseDefaultTargetUrl() || (targetUrlParameter != null && StringUtils.hasText(request .getParameter(targetUrlParameter)))) { requestCache.removeRequest(request, response); super.onAuthenticationSuccess(request, response, authentication); return; } clearAuthenticationAttributes(request); // Use the DefaultSavedRequest URL String targetUrl = savedRequest.getRedirectUrl(); logger.debug(“Redirecting to DefaultSavedRequest Url: " + targetUrl); getRedirectStrategy().sendRedirect(request, response, targetUrl);}其实,这个方法,就是获取到之前保存的请求信息,然后再重定向到之前的请求。#### 2.CAS登录流程分析这次,我们访问租户b,这个租户,配置了cas登录。访问租户b:<http://localhost:8085/> ,这个页面里,也就是一个超链接,点击超链接,访问http://localhost:8083/oauth/authorize?client_id=iqiyi&response_type=token&redirect_uri=http://localhost:8081/iqiyi/qq/redirect前面的流程还是一样的,经过spring security的过滤器链,都没有匹配到,在最后DispatcherServlet抛出异常,然后ExceptionTranslationFilter对异常处理,跳转到/login端点,然后拿出配置在数据库中的casInfo,跳转到https://localhost:8443/cas/login?service=http%3A%2F%2Flocalhost%3A8083%2Flogin%2Fcas输入用户名密码,cas成功认证用户之后,生成TGT=============================================================WHO: adminWHAT: Supplied credentials: [admin]ACTION: AUTHENTICATION_SUCCESSAPPLICATION: CASWHEN: Fri Apr 19 16:51:01 CST 2019CLIENT IP ADDRESS: 0:0:0:0:0:0:0:1SERVER IP ADDRESS: 0:0:0:0:0:0:0:12019-04-19 16:51:01,300 INFO [org.apereo.inspektr.audit.support.Slf4jLoggingAuditTrailManager] - <Audit trail record BEGINWHO: adminWHAT: TGT-GHfz0lUJQE-8fkKJgyv8WXNE5FYLBqb7zfWGfNoKwDZ0AjqA-DESKTOP-GDU9JIIACTION: TICKET_GRANTING_TICKET_CREATEDAPPLICATION: CASWHEN: Fri Apr 19 16:51:01 CST 2019CLIENT IP ADDRESS: 0:0:0:0:0:0:0:1SERVER IP ADDRESS: 0:0:0:0:0:0:0:12019-04-19 16:51:01,307 INFO [org.apereo.cas.DefaultCentralAuthenticationService] - <Granted ticket [ST-35-Mf1v9Z2qVVVKlWeTgyc-Hlzh2xY-DESKTOP-GDU9JII] for service [http://localhost:8083/login/cas] and principal [admin]>2019-04-19 16:51:01,308 INFO [org.apereo.inspektr.audit.support.Slf4jLoggingAuditTrailManager] - <Audit trail record BEGINWHO: adminWHAT: ST-35-Mf1v9Z2qVVVKlWeTgyc-Hlzh2xY-DESKTOP-GDU9JII for http://localhost:8083/login/casACTION: SERVICE_TICKET_CREATEDAPPLICATION: CASWHEN: Fri Apr 19 16:51:01 CST 2019CLIENT IP ADDRESS: 0:0:0:0:0:0:0:1SERVER IP ADDRESS: 0:0:0:0:0:0:0:1然后跳转到service地址,也就是localhost:8083/login/cas ,并带上为这个service生成的service ticket,所以最后的请求地址为:http://localhost:8083/login/cas?ticket=ST-35-Mf1v9Z2qVVVKlWeTgyc-Hlzh2xY-DESKTOP-GDU9JII而这个端点/login/cas会被我配置的自定义CustomCasAuthenticationFilter拦截@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; if (!requiresAuthentication(req, res)) { chain.doFilter(request, response); return; } String ticket = obtainArtifact(req); //开始校验ticket try { CasInfo casInfo = (CasInfo) req.getSession().getAttribute(“casInfoByTenantId”); if (StringUtils.hasText(casInfo.getCasServer())) { //获取当前项目地址 String service; int port = request.getServerPort(); if (port != 80) { service = request.getScheme() + “://” + request.getServerName() + “:” + request.getServerPort() + endpoint; } else { service = request.getScheme() + “://” + request.getServerName() + endpoint; } //开始校验ticket Assertion validateResult = getTicketValidator(casInfo.getCasServer()).validate(ticket, service); //根据校验结果,获取用户详细信息 UserDetails userDetails = null; try { userDetails = userDetailsService.loadUserByUsername(validateResult.getPrincipal().getName()); if (this.logger.isDebugEnabled()) { logger.debug(“userDetailsServiceImpl is loading username:"+validateResult.getPrincipal().getName()); } } catch (UsernameNotFoundException e) { unsuccessfulAuthentication(req, res, e); } //手动封装authentication对象 assert userDetails != null; UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(validateResult.getPrincipal(), ticket, userDetails.getAuthorities()); authentication.setDetails(userDetails); successfulAuthentication(req,res,chain,authentication); } else { unsuccessfulAuthentication(req, res, new BadCredentialsException(“bad credential:ticket校验失败”)); } } catch (TicketValidationException e) { //ticket校验失败 unsuccessfulAuthentication(req, res, new BadCredentialsException(e.getMessage())); }// chain.doFilter(request, response);}校验成功之后,我的逻辑是,手动加载用户信息,然后把当前认证信息Authentication放到SecurityContextHolder中。protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { if (this.logger.isDebugEnabled()) { this.logger.debug(“Authentication success. Updating SecurityContextHolder to contain: " + authResult); } SecurityContextHolder.getContext().setAuthentication(authResult); if (this.eventPublisher != null) { this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass())); } this.successHandler.onAuthenticationSuccess(request, response, authResult);}protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { SecurityContextHolder.clearContext(); if (this.logger.isDebugEnabled()) { this.logger.debug(“Authentication request failed: " + failed.toString(), failed); this.logger.debug(“Updated SecurityContextHolder to contain null Authentication”); this.logger.debug(“Delegating to authentication failure handler " + this.failureHandler); } this.failureHandler.onAuthenticationFailure(request, response, failed);}#### 3.单点登出流程分析用户发送/logout请求,被我自定义的CustomLogoutFilter拦截之后的逻辑是,先从本地登出,然后判断之前是否是从cas认证的,如果是,再获取cas信息,然后把cas也登出了。这里判断登陆用户的认证方式,我想了很久,最后的实现思路如下:之前通过cas登录时,我手动的添加登陆用户的认证方式到Authentication中。代码如下://根据校验结果,获取用户详细信息UserDetails userDetails = null;try {userDetails = userDetailsService.loadUserByUsername(validateResult.getPrincipal().getName());if (this.logger.isDebugEnabled()) { logger.debug(“userDetailsServiceImpl is loading username:"+validateResult.getPrincipal().getName());}} catch (UsernameNotFoundException e) {unsuccessfulAuthentication(req, res, e);}//手动封装authentication对象assert userDetails != null;UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(validateResult.getPrincipal(), ticket, userDetails.getAuthorities());//就是这里做了文章authentication.setDetails(userDetails);successfulAuthentication(req,res,chain,authentication);然后,登出时,拿到这个信息,进行登出操作。因为,我在userdetails中封装了这个信息,所以可以拿到。public class UserDetailsVO implements UserDetails {//userprivate Integer userId;private String username;private String phone;private String email;//tenantprivate Tenant tenant;//accountprivate Integer accountId;private String password;//省略setter和getter} ...

April 19, 2019 · 11 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 webflux 中实现 RequestContextHolder

说明在 Spring boot web 中我们可以通过 RequestContextHolder 很方便的获取 request。ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();// 获取 requestHttpServletRequest request = requestAttributes.getRequest();不再需要通过参数传递 request。在 Spring webflux 中并没提供该功能,使得我们在 Aop 或者一些其他的场景中获取 request 变成了一个奢望???寻求解决方案首先我想到的是看看 spring-security 中是否有对于的解决方案,因为在 spring-security 中我们也是可以通过 SecurityContextHolder 很方便快捷的获取当前登录的用户信息。找到了 ReactorContextWebFilter,我们来看看 security 中他是怎么实现的。https://github.com/spring-pro…public class ReactorContextWebFilter implements WebFilter { private final ServerSecurityContextRepository repository; public ReactorContextWebFilter(ServerSecurityContextRepository repository) { Assert.notNull(repository, “repository cannot be null”); this.repository = repository; } @Override public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { return chain.filter(exchange) .subscriberContext(c -> c.hasKey(SecurityContext.class) ? c : withSecurityContext(c, exchange) ); } private Context withSecurityContext(Context mainContext, ServerWebExchange exchange) { return mainContext.putAll(this.repository.load(exchange) .as(ReactiveSecurityContextHolder::withSecurityContext)); }}源码里面我们可以看到 他利用一个 Filter,chain.filter(exchange) 的返回值 Mono 调用了 subscriberContext 方法。那么我们就去了解一下这个 reactor.util.context.Context。找到 reactor 官方文档中的 context 章节:https://projectreactor.io/doc…大意是:从 Reactor 3.1.0 开始提供了一个高级功能,可以与 ThreadLocal 媲美,应用于 Flux 和 Mono 的上下文工具 Context。更多请大家查阅官方文档,对英文比较抵触的朋友可以使用 google 翻译。mica 中的实现mica 中的实现比较简单,首先是我们的 ReactiveRequestContextFilter:/** * ReactiveRequestContextFilter * * @author L.cm /@Configuration@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)public class ReactiveRequestContextFilter implements WebFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); return chain.filter(exchange) .subscriberContext(ctx -> ctx.put(ReactiveRequestContextHolder.CONTEXT_KEY, request)); }}在 Filter 中直接将 request 存储到 Context 上下文中。ReactiveRequestContextHolder 工具:/* * ReactiveRequestContextHolder * * @author L.cm /public class ReactiveRequestContextHolder { static final Class<ServerHttpRequest> CONTEXT_KEY = ServerHttpRequest.class; /* * Gets the {@code Mono<ServerHttpRequest>} from Reactor {@link Context} * @return the {@code Mono<ServerHttpRequest>} */ public static Mono<ServerHttpRequest> getRequest() { return Mono.subscriberContext() .map(ctx -> ctx.get(CONTEXT_KEY)); }}怎么使用呢?mica 中对未知异常处理,从 request 中获取请求的相关信息@ExceptionHandler(Throwable.class)@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)public Mono<?> handleError(Throwable e) { log.error(“未知异常”, e); // 发送:未知异常异常事件 return ReactiveRequestContextHolder.getRequest() .doOnSuccess(r -> publishEvent(r, e)) .flatMap(r -> Mono.just(R.fail(SystemCode.FAILURE)));}private void publishEvent(ServerHttpRequest request, Throwable error) { // 具体业务逻辑}WebClient 透传 request 中的 header此示例来源于开源中国问答中笔者的回复: 《如何在gateway 中获取 webflux的 RequestContextHolder》@GetMapping("/test")@ResponseBodypublic Mono<String> test() { WebClient webClient = testClient(); return webClient.get().uri("").retrieve().bodyToMono(String.class);}@Beanpublic WebClient testClient() { return WebClient.builder() .filter(testFilterFunction()) .baseUrl(“https://www.baidu.com”) .build();}private ExchangeFilterFunction testFilterFunction() { return (request, next) -> ReactiveRequestContextHolder.getRequest() .flatMap(r -> { ClientRequest clientRequest = ClientRequest.from(request) .headers(headers -> headers.set(HttpHeaders.USER_AGENT, r.getHeaders().getFirst(HttpHeaders.USER_AGENT))) .build(); return next.exchange(clientRequest); });}上段代码是透传 web 中的 request 中的 user_agent 请求头到 WebClient 中。开源推荐mica Spring boot 微服务核心组件集:https://gitee.com/596392912/micaAvue 一款基于vue可配置化的神奇框架:https://gitee.com/smallweigit/avuepig 宇宙最强微服务(架构师必备):https://gitee.com/log4j/pigSpringBlade 完整的线上解决方案(企业开发必备):https://gitee.com/smallc/SpringBladeIJPay 支付SDK让支付触手可及:https://gitee.com/javen205/IJPay关注我们扫描上面二维码,更多精彩内容每天推荐!转载声明如梦技术对此篇文章有最终所有权,转载请注明出处,参考也请注明,谢谢! ...

April 4, 2019 · 2 min · jiezi

扩展资源服务器解决oauth2 性能瓶颈

用户携带token 请求资源服务器资源服务器拦截器 携带token 去认证服务器 调用tokenstore 对token 合法性校验资源服务器拿到token,默认只会含有用户名信息通过用户名调用userdetailsservice.loadbyusername 查询用户全部信息详细性能瓶颈分析,请参考上篇文章《扩展jwt解决oauth2 性能瓶颈》 本文是针对传统使用UUID token 的情况进行扩展,提高系统的吞吐率,解决性能瓶颈的问题默认check-token 解析逻辑RemoteTokenServices 入口@Overridepublic OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException { MultiValueMap<String, String> formData = new LinkedMultiValueMap<String, String>(); formData.add(tokenName, accessToken); HttpHeaders headers = new HttpHeaders(); headers.set(“Authorization”, getAuthorizationHeader(clientId, clientSecret)); // 调用认证服务器的check-token 接口检查token Map<String, Object> map = postForMap(checkTokenEndpointUrl, formData, headers); return tokenConverter.extractAuthentication(map);}解析认证服务器返回的信息DefaultAccessTokenConverterpublic OAuth2Authentication extractAuthentication(Map<String, ?> map) { Map<String, String> parameters = new HashMap<String, String>(); Set<String> scope = extractScope(map); // 主要是 用户的信息的抽取 Authentication user = userTokenConverter.extractAuthentication(map); // 一些oauth2 信息的填充 OAuth2Request request = new OAuth2Request(parameters, clientId, authorities, true, scope, resourceIds, null, null, null); return new OAuth2Authentication(request, user); }组装当前用户信息DefaultUserAuthenticationConverterpublic Authentication extractAuthentication(Map<String, ?> map) { if (map.containsKey(USERNAME)) { Object principal = map.get(USERNAME); Collection<? extends GrantedAuthority> authorities = getAuthorities(map); if (userDetailsService != null) { UserDetails user = userDetailsService.loadUserByUsername((String) map.get(USERNAME)); authorities = user.getAuthorities(); principal = user; } return new UsernamePasswordAuthenticationToken(principal, “N/A”, authorities); } return null;}问题分析认证服务器check-token 返回的全部信息资源服务器在根据返回信息组装用户信息的时候,只是用了username如果设置了 userDetailsService 的实现则去调用 loadUserByUsername 再去查询一次用户信息造成问题现象如果设置了userDetailsService 即可在spring security 上下文获取用户的全部信息,不设置则只能得到用户名。增加了一次查询逻辑,对性能产生不必要的影响解决问题扩展UserAuthenticationConverter 的解析过程,把认证服务器返回的信息全部组装到spring security的上下文对象中/** * @author lengleng * @date 2019-03-07 * <p> * 根据checktoken 的结果转化用户信息 */public class PigxUserAuthenticationConverter implements UserAuthenticationConverter { private static final String N_A = “N/A”; // map 是check-token 返回的全部信息 @Override public Authentication extractAuthentication(Map<String, ?> map) { if (map.containsKey(USERNAME)) { Collection<? extends GrantedAuthority> authorities = getAuthorities(map); String username = (String) map.get(USERNAME); Integer id = (Integer) map.get(SecurityConstants.DETAILS_USER_ID); Integer deptId = (Integer) map.get(SecurityConstants.DETAILS_DEPT_ID); Integer tenantId = (Integer) map.get(SecurityConstants.DETAILS_TENANT_ID); PigxUser user = new PigxUser(id, deptId, tenantId, username, N_A, true , true, true, true, authorities); return new UsernamePasswordAuthenticationToken(user, N_A, authorities); } return null; }}给remoteTokenServices 注入这个实现public class PigxResourceServerConfigurerAdapter extends ResourceServerConfigurerAdapter { @Override public void configure(ResourceServerSecurityConfigurer resources) { DefaultAccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter(); UserAuthenticationConverter userTokenConverter = new PigxUserAuthenticationConverter(); accessTokenConverter.setUserTokenConverter(userTokenConverter); remoteTokenServices.setRestTemplate(lbRestTemplate); remoteTokenServices.setAccessTokenConverter(accessTokenConverter); resources. .tokenServices(remoteTokenServices); }}完成扩展,再来看文章开头的流程图就变成了如下关注我个人项目 基于Spring Cloud、OAuth2.0开发基于Vue前后分离的开发平台QQ: 2270033969 一起来聊聊你们是咋用 spring cloud 的吧。 ...

March 20, 2019 · 2 min · jiezi

SpringBoot 实战 (十七) | 整合 WebSocket 实现聊天室

微信公众号:一个优秀的废人。如有问题,请后台留言,反正我也不会听。前言昨天那篇介绍了 WebSocket 实现广播,也即服务器端有消息时,将消息发送给所有连接了当前 endpoint 的浏览器。但这无法解决消息由谁发送,又由谁接收的问题。所以,今天写一篇实现一对一的聊天室。今天这一篇建立在昨天那一篇的基础之上,为便于更好理解今天这一篇,推荐先阅读:「SpringBoot 整合WebSocket 实现广播消息 」准备工作Spring Boot 2.1.3 RELEASESpring Security 2.1.3 RELEASEIDEAJDK8pom 依赖因聊天室涉及到用户相关,所以在上一篇基础上引入 Spring Security 2.1.3 RELEASE 依赖<!– Spring Security 依赖 –><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency>Spring Security 的配置虽说涉及到 Spring Security ,但鉴于篇幅有限,这里只对这个项目相关的部分进行介绍,具体的 Spring Security 教程,后面会出。这里的 Spring Security 配置很简单,具体就是设置登录路径、设置安全资源以及在内存中创建用户和密码,密码需要注意加密,这里使用 BCrypt 加密算法在用户登录时对密码进行加密。 代码注释很详细,不多说。package com.nasus.websocket.config;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.builders.WebSecurity;import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;@Configuration// 开启Spring Security的功能@EnableWebSecuritypublic class WebSecurityConfig extends WebSecurityConfigurerAdapter{ @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 设置 SpringSecurity 对 / 和 “/login” 路径不拦截 .mvcMatchers("/","/login").permitAll() .anyRequest().authenticated() .and() .formLogin() // 设置 Spring Security 的登录页面访问路径为/login .loginPage("/login") // 登录成功后转向 /chat 路径 .defaultSuccessUrl("/chat") .permitAll() .and() .logout() .permitAll(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() // 在内存中分配两个用户 nasus 和 chenzy ,用户名和密码一致 // BCryptPasswordEncoder() 是 Spring security 5.0 中新增的加密方式 // 登陆时用 BCrypt 加密方式对用户密码进行处理。 .passwordEncoder(new BCryptPasswordEncoder()) .withUser(“nasus”) // 保证用户登录时使用 bcrypt 对密码进行处理再与内存中的密码比对 .password(new BCryptPasswordEncoder().encode(“nasus”)).roles(“USER”) .and() // 登陆时用 BCrypt 加密方式对用户密码进行处理。 .passwordEncoder(new BCryptPasswordEncoder()) .withUser(“chenzy”) // 保证用户登录时使用 bcrypt 对密码进行处理再与内存中的密码比对 .password(new BCryptPasswordEncoder().encode(“chenzy”)).roles(“USER”); } @Override public void configure(WebSecurity web) throws Exception { // /resource/static 目录下的静态资源,Spring Security 不拦截 web.ignoring().antMatchers("/resource/static**"); }}WebSocket 的配置在上一篇的基础上另外注册一个名为 “/endpointChat” 的节点,以供用户订阅,只有订阅了该节点的用户才能接收到消息;然后,再增加一个名为 “/queue” 消息代理。@Configuration// @EnableWebSocketMessageBroker 注解用于开启使用 STOMP 协议来传输基于代理(MessageBroker)的消息,这时候控制器(controller)// 开始支持@MessageMapping,就像是使用 @requestMapping 一样。@EnableWebSocketMessageBrokerpublic class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { //注册一个名为 /endpointNasus 的 Stomp 节点(endpoint),并指定使用 SockJS 协议。 registry.addEndpoint("/endpointNasus").withSockJS(); //注册一个名为 /endpointChat 的 Stomp 节点(endpoint),并指定使用 SockJS 协议。 registry.addEndpoint("/endpointChat").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { // 广播式配置名为 /nasus 消息代理 , 这个消息代理必须和 controller 中的 @SendTo 配置的地址前缀一样或者全匹配 // 点对点增加一个 /queue 消息代理 registry.enableSimpleBroker("/queue","/nasus/getResponse"); }}控制器 controller指定发送消息的格式以及模板。详情见,代码注释。@Autowired//使用 SimpMessagingTemplate 向浏览器发送信息private SimpMessagingTemplate messagingTemplate;@MessageMapping("/chat")public void handleChat(Principal principal,String msg){ // 在 SpringMVC 中,可以直接在参数中获得 principal,principal 中包含当前用户信息 if (principal.getName().equals(“nasus”)){ // 硬编码,如果发送人是 nasus 则接收人是 chenzy 反之也成立。 // 通过 messageingTemplate.convertAndSendToUser 方法向用户发送信息,参数一是接收消息用户,参数二是浏览器订阅地址,参数三是消息本身 messagingTemplate.convertAndSendToUser(“chenzy”, “/queue/notifications”,principal.getName()+"-send:" + msg); } else { messagingTemplate.convertAndSendToUser(“nasus”, “/queue/notifications”,principal.getName()+"-send:" + msg); }}登录页面<!DOCTYPE html><html xmlns=“http://www.w3.org/1999/xhtml" xmlns:th=“http://www.thymeleaf.org” xmlns:sec=“http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"><meta charset=“UTF-8” /><head> <title>登陆页面</title></head><body><div th:if="${param.error}"> 无效的账号和密码</div><div th:if="${param.logout}"> 你已注销</div><form th:action=”@{/login}” method=“post”> <div><label> 账号 : <input type=“text” name=“username”/> </label></div> <div><label> 密码: <input type=“password” name=“password”/> </label></div> <div><input type=“submit” value=“登陆”/></div></form></body></html>聊天页面<!DOCTYPE html><html xmlns:th=“http://www.thymeleaf.org”><meta charset=“UTF-8” /><head> <title>Home</title> <script th:src="@{sockjs.min.js}"></script> <script th:src="@{stomp.min.js}"></script> <script th:src="@{jquery.js}"></script></head><body><p> 聊天室</p><form id=“nasusForm”> <textarea rows=“4” cols=“60” name=“text”></textarea> <input type=“submit”/></form><script th:inline=“javascript”> $(’#nasusForm’).submit(function(e){ e.preventDefault(); var text = $(’#nasusForm’).find(’textarea[name=“text”]’).val(); sendSpittle(text); }); // 连接 SockJs 的 endpoint 名称为 “/endpointChat” var sock = new SockJS("/endpointChat"); var stomp = Stomp.over(sock); stomp.connect(‘guest’, ‘guest’, function(frame) { // 订阅 /user/queue/notifications 发送的消息,这里与在控制器的 // messagingTemplate.convertAndSendToUser 中订阅的地址保持一致 // 这里多了 /user 前缀,是必须的,使用了 /user 才会把消息发送到指定用户 stomp.subscribe("/user/queue/notifications", handleNotification); }); function handleNotification(message) { $(’#output’).append("<b>Received: " + message.body + “</b><br/>”) } function sendSpittle(text) { stomp.send("/chat", {}, text); } $(’#stop’).click(function() {sock.close()});</script><div id=“output”></div></body></html>页面控制器 controller@Controllerpublic class ViewController { @GetMapping("/nasus") public String getView(){ return “nasus”; } @GetMapping("/login") public String getLoginView(){ return “login”; } @GetMapping("/chat") public String getChatView(){ return “chat”; }}测试预期结果应该是:两个用户登录系统,可以互相发送消息。但是同一个浏览器的用户会话的 session 是共享的,这里需要在 Chrome 浏览器再添加一个用户。具体操作在 Chrome 的 设置–>管理用户–>添加用户:两个用户分别访问 http://localhost:8080/login 登录系统,跳转至聊天界面:相互发送消息:完整代码https://github.com/turoDog/De…如果觉得对你有帮助,请给个 Star 再走呗,非常感谢。后语如果本文对你哪怕有一丁点帮助,请帮忙点好看。你的好看是我坚持写作的动力。另外,关注之后在发送 1024 可领取免费学习资料。资料详情请看这篇旧文:Python、C++、Java、Linux、Go、前端、算法资料分享 ...

March 6, 2019 · 2 min · jiezi

Spring Security项目构建(一)

源码地址Github项目构建依赖关系代码结构security:主模块security-core:核心业务逻辑security-browser:浏览器安全特定代码security-app:app相关特定代码security-demo:样例程序包引入主模块<?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> <groupId>com.guosh.security</groupId> <artifactId>guosh-security</artifactId> <packaging>pom</packaging> <version>1.0-SNAPSHOT</version> <!–声明变量–> <properties> <guosh.security.version>1.0-SNAPSHOT</guosh.security.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>io.spring.platform</groupId> <artifactId>platform-bom</artifactId> <version>Brussels-SR16</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Dalston.SR5</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <!–<build>–> <!–<plugins>–> <!–<plugin>–> <!–<groupId>org.apache.maven.plugins</groupId>–> <!–<artifactId>maven-compiler-plugin</artifactId>–> <!–<version>2.3.2</version>–> <!–<configuration>–> <!–<source>1.8</source> &lt;!&ndash; 源代码使用的开发版本 &ndash;&gt;–> <!–<target>1.8</target> &lt;!&ndash; 需要生成的目标class文件的编译版本 &ndash;&gt;–> <!–<encoding>UTF-8</encoding>–> <!–</configuration>–> <!–</plugin>–> <!–</plugins>–> <!–</build>–> <modules> <module>guosh-security-core</module> <module>guosh-security-browser</module> <module>guosh-security-demo</module> <module>guosh-security-app</module> </modules></project>core模块<?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"> <parent> <artifactId>guosh-security</artifactId> <groupId>com.guosh.security</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>guosh-security-core</artifactId> <dependencies> <!–oauth2认证–> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!–第三方登陆–> <dependency> <groupId>org.springframework.social</groupId> <artifactId>spring-social-config</artifactId> </dependency> <dependency> <groupId>org.springframework.social</groupId> <artifactId>spring-social-core</artifactId> </dependency> <dependency> <groupId>org.springframework.social</groupId> <artifactId>spring-social-security</artifactId> </dependency> <dependency> <groupId>org.springframework.social</groupId> <artifactId>spring-social-web</artifactId> </dependency> <!–工具类–> <dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> </dependency> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> </dependency> <dependency> <groupId>commons-beanutils</groupId> <artifactId>commons-beanutils</artifactId> </dependency> </dependencies></project>browser模块<?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"> <parent> <artifactId>guosh-security</artifactId> <groupId>com.guosh.security</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>guosh-security-browser</artifactId> <dependencies> <dependency> <groupId>com.guosh.security</groupId> <artifactId>guosh-security-core</artifactId> <version>${guosh.security.version}</version> </dependency> <!–session集群–> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session</artifactId> </dependency> </dependencies></project>app 模块<?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"> <parent> <artifactId>guosh-security</artifactId> <groupId>com.guosh.security</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>guosh-security-app</artifactId> <dependencies> <dependency> <groupId>com.guosh.security</groupId> <artifactId>guosh-security-core</artifactId> <version>${guosh.security.version}</version> </dependency> </dependencies></project>demo 模块<?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"> <parent> <artifactId>guosh-security</artifactId> <groupId>com.guosh.security</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>guosh-security-demo</artifactId> <dependencies> <dependency> <groupId>com.guosh.security</groupId> <artifactId>guosh-security-browser</artifactId> <version>${guosh.security.version}</version> </dependency> </dependencies> <build> <!–demo是web程序需要运行打完包的名字–> <finalName>guoshsecurity</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>1.3.3.RELEASE</version> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build></project>Hello程序现在需要把项目启动起来所以做了一个Demo1.在Demo下面新建yml文件并连接数据库server: port: 8080 context-path: /guoshsecurityspring: datasource: druid: url: jdbc:mysql://localhost:3306/guosecurity?useUnicode=true&characterEncoding=utf8&useSSL=true username: root password: root driver-class-name: com.mysql.jdbc.Driver #连接池初始化大小最小最大 initial-size: 2 min-idle: 2 max-active: 2 # 配置获取连接等待超时的时间 maxWait: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 timeBetweenEvictionRunsMillis: 60000 # 配置一个连接在池中最小生存的时间,单位是毫秒 minEvictableIdleTimeMillis: 60000 testWhileIdle: true testOnBorrow: false testOnReturn: false # 打开PSCache,并且指定每个连接上PSCache的大小 poolPreparedStatements: true maxPoolPreparedStatementPerConnectionSize: 20 # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,‘wall’用于防火墙 filters: stat,wall,log4j #属性来打开mergeSql功能;慢SQL记录 connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 #验证连接是否可用,使用的SQL语句 validation-query: SELECT 1 #合并多个DruidDataSource的监控数据 useGlobalDataSourceStat: true #监控配置 web-stat-filter: url-pattern: /* exclusions: /druid/,.js,.gif,.jpg,.png,.css,.ico stat-view-servlet: url-pattern: /druid/ login-username: guoadmin login-password: guoadmin #是否能按重制按键 reset-enable: true #关闭spring session session: store-type: none#先关闭security默认配置security: basic: enabled: false2.新建Spring boot的启动文件,添加一个/hello测试3.执行main函数查看4.打包测试使用maven打包命令生成jar包之后可以使用java -jar ./guoshsecurity.jar 启动 ...

March 4, 2019 · 2 min · jiezi

Spring Security and Angular 实现用户认证

引言度过了前端框架的技术选型之后,新系统起步。ng-alain,一款功能强大的前端框架,设计得很好,两大缺点,文档不详尽,框架代码不规范。写前台拦截器的时候是在花了大约半小时的时间对代码进行全面规范化之后才开始进行的。又回到了最原始的问题,认证授权,也就是Security。认证授权认证,也就是判断用户是否可登录系统。授权,用户登录系统后可以干什么,哪些操作被允许。本文,我们使用Spring Security与Angular进行用户认证。开发环境Java 1.8Spring Boot 2.0.5.RELEASE学习这里给大家介绍一下我学习用户认证的经历。官方文档第一步,肯定是想去看官方文档,Spring Security and Angular - Spring.io。感叹一句这个文档,实在是太长了!!!记得当时看这个文档看了一晚上,看完还不敢睡觉,一鼓作气写完,就怕第二天起来把学得都忘了。我看完这个文档,其实我们需要的并不是文档的全部。总结一下文档的结构:引言讲解前后台不分离项目怎么使用basic方式登录前后台不分离项目怎么使用form方式登录,并自定义登录表单讲解CSRF保护(这块没看懂,好像就是防止伪造然后多存一个X-XSRF-TOKEN)修改架构,启用API网关进行转发(计量项目原实现方式)使用Spring Session自定义token实现Oauth2登录文档写的很好,讲解了许多why?,我们为什么要这么设计。我猜想这篇文章应该默认学者已经掌握Spring Security,反正我零基础看着挺费劲的。初学建议结合IBM开发者社区上的博客进行学习(最近才发现的,上面写的都特别好,有的作者怕文字说不明白的还特意录了个视频放在上面)。学习 - IBM中国这是我结合学习的文章:Spring Security 的 Web 应用和指纹登录实践实现引入Security依赖<!– Security –><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency>基础配置继承配置适配器WebSecurityConfigurerAdapter,就实现了Spring Security的配置。重写configure,自定义认证规则。注意,configure里的代码不要当成代码看,否则会死得很惨。就把他当成普通的句子看!!!@Componentpublic class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // 使用Basic认证方式进行验证进行验证 .httpBasic() // 要求SpringSecurity对后台的任何请求进行认证保护 .and().authorizeRequests().anyRequest().authenticated(); }}如此,我们后台的接口就被Spring Security保护起来了,当访问接口时,浏览器会弹出登录提示框。用户名是user,密码已打印在控制台:自定义认证这不行呀,不可能项目一上线,用的还是随机生成的用户名和密码,应该去数据库里查。实现UserDetailsService接口并交给Spring托管,在用户认证时,Spring Security即自动调用我们实现的loadUserByUsername方法,传入username,然后再用我们返回的对象进行其他认证操作。该方法要求我们根据我们自己的User来构造Spring Security内置的org.springframework.security.core.userdetails.User,如果抛出UsernameNotFoundException,则Spring Security代替我们返回401。@Componentpublic class YunzhiAuthService implements UserDetailsService { @Autowired private UserRepository userRepository; private static final Logger logger = LoggerFactory.getLogger(YunzhiAuthService.class); @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { logger.debug(“根据用户名查询用户”); User user = userRepository.findUserByUsername(username); logger.debug(“用户为空,则抛出异常”); if (user == null) { throw new UsernameNotFoundException(“用户名不存在”); } // TODO: 学习Spring Security中的role授权,看是否对项目有所帮助 return new org.springframework.security.core.userdetails.User(username, user.getPassword(), AuthorityUtils.createAuthorityList(“admin”)); }}基础的代码大家都能看懂,这里讲解一下最后一句。return new org.springframework.security.core.userdetails.User(username, user.getPassword(), AuthorityUtils.createAuthorityList(“admin”));构建一个用户,用户名密码都是我们查出来set进去的,对该用户授权admin角色(暂且这么写,这个对用户授予什么角色关系到授权,我们日后讨论)。然后Spring Security就调用我们返回的User对象进行密码判断与用户授权。用户冻结Spring Security只有用户名和密码认证吗?那用户冻结了怎么办呢?这个无须担心,点开org.springframework.security.core.userdetails.User,一个三个参数的构造函数,一个七个参数的构造函数,去看看源码中的注释,一切都不是问题。Spring Security设计得相当完善。public User(String username, String password, Collection<? extends GrantedAuthority> authorities) { this(username, password, true, true, true, true, authorities);}public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) { if (((username == null) || “".equals(username)) || (password == null)) { throw new IllegalArgumentException( “Cannot pass null or empty values to constructor”); } this.username = username; this.password = password; this.enabled = enabled; this.accountNonExpired = accountNonExpired; this.credentialsNonExpired = credentialsNonExpired; this.accountNonLocked = accountNonLocked; this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));}启用密码加密忘了当时是什么场景了,好像是写完YunzhiAuthService之后再启动项目,控制台中就有提示:具体内容记不清了,大体意思就是推荐我采用密码加密。特意查了一下数据库中的密码需不需要加密,然后就查到了CSDN的密码泄露事件,很多开发者都批判CSDN的程序员,说明文存储密码是一种非常不服责任的行为。然后又搜到了腾讯有关的一些文章,反正密码加密了,数据泄露了也不用承担过多的法律责任。腾讯还是走在法律的前列啊,话说是不是腾讯打官司还没输过?既然这么多人都推荐加密,那我们也用一用吧。去Google了一下查了,好像BCryptPasswordEncoder挺常用的,就添加到上下文里了,然后Spring Security再进行密码判断的话,就会把传来的密码经过BCryptPasswordEncoder加密,判断和我们传给它的加密密码是否一致。@Configurationpublic class BeanConfig { /** * 密码加密 / @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }}然后一些User的细节就参考李宜衡的文章:Hibernate实体监听器。Help, How is My Application Going to Scale?其实,如果对技术要求不严谨的人来说,上面已经足够了。如果你也有一颗崇尚技术的心,我们一起往下看。嘿!我的应用程序怎么扩大规模?这是Spring官方文档中引出的话题,官方文档中对这一块的描述过于学术,什么TCP,什么stateless。说实话,这段我看了好几遍也没看懂,但是我非常同意这个结论:我们不能用Spring Security帮我们管理Session。以下是我个人的观点:因为这是存在本地的,当我们的后台有好多台服务器,怎么办?用户这次请求的是Server1,Server1上存了一个seesion,然后下次请求的是Server2,Server2没有session,完了,401。所以我们要禁用Spring Security的Session,但是手动管理Session又太复杂,所以引入了新项目:Spring Session。Spring Session的一大优点也是支持集群Session。引入Spring Session<!– Redis –><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId></dependency><!– Session –><dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId></dependency>这里引入的是Spring Session中的Session-Redis项目,使用Redis服务器存储Session,实现集群共享。禁用Spring Security的Session管理@Componentpublic class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // 使用Basic认证方式进行验证进行验证 .httpBasic() // 要求SpringSecurity对后台的任何请求进行认证保护 .and().authorizeRequests().anyRequest().authenticated() // 关闭Security的Session,使用Spring Session .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER); }}关闭Spring Security的Session管理,设置Session创建策略为NEVER。Spring Security will never create an HttpSession, but will use the HttpSession if it already existsSpring Security不会创建HttpSession,但是如果存在,会使用这个HttpSession。启用Redis管理SessionMac下使用Homebrew安装redis十分简单,Mac下安装配置Redis。@EnableRedisHttpSession@Configurationpublic class BeanConfig { /* * 设置Session的token策略 / @Bean public HeaderHttpSessionIdResolver httpSessionIdResolver() { return new HeaderHttpSessionIdResolver(“token”); }}@EnableRedisHttpSession启用Redis的Session管理,上下文中加入对象HeaderHttpSessionIdResolver,设置从Http请求中找header里的token最为认证字段。梳理逻辑很乱是吗?让我们重新梳理一下逻辑。使用HttpBasic方式登录,用户名和密码传给后台,Spring Security进行用户认证,然后根据我们的配置,Spring Security使用的是Spring Session创建的Session,最后存入Redis。以后呢?登录之后,就是用token的方式进行用户认证,将token添加到header中,然后请求的时候后台识别header里的token进行用户认证。所以,我们需要在用户登录的时候返回token作为以后用户认证的条件。登录方案登录方案,参考官方文档学来的,很巧妙。以Spring的话来说:这个叫trick,小骗术。我们的login方法长成这样:@GetMapping(“login”)public Map<String, String> login(@AuthenticationPrincipal Principal user, HttpSession session) { logger.info(“用户: " + user.getName() + “登录系统”); return Collections.singletonMap(“token”, session.getId());}简简单单的四行,就实现了后台的用户认证。原理因为我们的后台是受Spring Security保护的,所以当访问login方法时,就需要进行用户认证,认证成功才能执行到login方法。换句话说,只要我们的login方法执行到了,那就说明用户认证成功,所以login方法完全不需要业务逻辑,直接返回token,供之后认证使用。怎么样,是不是很巧妙?注销方案注销相当简单,直接清空当前的用户认证信息即可。@GetMapping(“logout”)public void logout(HttpServletRequest request, HttpServletResponse response) { logger.info(“用户注销”); // 获取用户认证信息 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); // 存在认证信息,注销 if (authentication != null) { new SecurityContextLogoutHandler().logout(request, response, authentication); }}单元测试如果对整个流程不是很明白的话,看下面的单元测试会有所帮助,代码很详尽,请理解整个认证的流程。@RunWith(SpringRunner.class)@SpringBootTest@AutoConfigureMockMvc@Transactionalpublic class AuthControllerTest { private static final Logger logger = LoggerFactory.getLogger(AuthControllerTest.class); private static final String LOGIN_URL = “/auth/login”; private static final String LOGOUT_URL = “/auth/logout”; private static final String TOKEN_KEY = “token”; @Autowired private MockMvc mockMvc; @Test public void securityTest() throws Exception { logger.debug(“初始化基础变量”); String username; String password; byte[] encodedBytes; MvcResult mvcResult; logger.debug(“1. 测试用户名不存在”); username = CommonService.getRandomStringByLength(10); password = “admin”; encodedBytes = Base64.encodeBase64((username + “:” + password).getBytes()); logger.debug(“断言401”); this.mockMvc.perform(MockMvcRequestBuilders.get(LOGIN_URL) .header(“Authorization”, “Basic " + new String(encodedBytes))) .andExpect(status().isUnauthorized()); logger.debug(“2. 用户名存在,但密码错误”); username = “admin”; password = CommonService.getRandomStringByLength(10); encodedBytes = Base64.encodeBase64((username + “:” + password).getBytes()); logger.debug(“断言401”); this.mockMvc.perform(MockMvcRequestBuilders.get(LOGIN_URL) .header(“Authorization”, “Basic " + new String(encodedBytes))) .andExpect(status().isUnauthorized()); logger.debug(“3. 用户名密码正确”); username = “admin”; password = “admin”; encodedBytes = Base64.encodeBase64((username + “:” + password).getBytes()); logger.debug(“断言200”); mvcResult = this.mockMvc.perform(MockMvcRequestBuilders.get(LOGIN_URL) .header(“Authorization”, “Basic " + new String(encodedBytes))) .andExpect(status().isOk()) .andReturn(); logger.debug(“从返回体中获取token”); String json = mvcResult.getResponse().getContentAsString(); JSONObject jsonObject = JSON.parseObject(json); String token = jsonObject.getString(“token”); logger.debug(“空的token请求后台,断言401”); this.mockMvc.perform(MockMvcRequestBuilders.get(LOGIN_URL) .header(TOKEN_KEY, “”)) .andExpect(status().isUnauthorized()); logger.debug(“加上token请求后台,断言200”); this.mockMvc.perform(MockMvcRequestBuilders.get(LOGIN_URL) .header(TOKEN_KEY, token)) .andExpect(status().isOk()); logger.debug(“用户注销”); this.mockMvc.perform(MockMvcRequestBuilders.get(LOGOUT_URL) .header(TOKEN_KEY, token)) .andExpect(status().isOk()); logger.debug(“注销后,断言该token失效”); this.mockMvc.perform(MockMvcRequestBuilders.get(LOGIN_URL) .header(TOKEN_KEY, token)) .andExpect(status().isUnauthorized()); }}前台方法和这么复杂的后台设计相比较,前台没有啥技术含量,把代码粘贴出来大家参考参考即可,没什么要说的。前台Service:@Injectable({ providedIn: ‘root’,})export class AuthService { constructor(private http: _HttpClient) { } /* * 登录 * @param username 用户名 * @param password 密码 / public login(username: string, password: string): Observable<ITokenModel> { // 新建Headers,并添加认证信息 let headers = new HttpHeaders(); headers = headers.append(‘Content-Type’, ‘application/x-www-form-urlencoded’); headers = headers.append(‘Authorization’, ‘Basic ’ + btoa(username + ‘:’ + password)); // 发起get请求并返回 return this.http .get(’/auth/login?_allow_anonymous=true’, {}, { headers: headers }); } /* * 注销 / public logout(): Observable<any> { return this.http.get(’/auth/logout’); }}登录组件核心代码:this.authService.login(this.userName.value, this.password.value) .subscribe((response: ITokenModel) => { // 清空路由复用信息 this.reuseTabService.clear(); // 设置用户Token信息 this.tokenService.set(response); // 重新获取 StartupService 内容,我们始终认为应用信息一般都会受当前用户授权范围而影响 this.startupSrv.load().then(() => { // noinspection JSIgnoredPromiseFromCall this.router.navigateByUrl(‘main/index’); }); }, () => { // 显示错误信息提示 this.showLoginErrorInfo = true; });注销组件核心代码:// 调用Service进行注销this.authService.logout().subscribe(() => { }, () => { }, () => { // 清空token信息 this.tokenService.clear(); // 跳转到登录页面,因为无论是否注销成功都要跳转,写在complete中 // noinspection JSIgnoredPromiseFromCall this.router.navigateByUrl(this.tokenService.login_url); });前台拦截器有一点,headers.append(‘X-Requested-With’, ‘XMLHttpRequest’),如果不设置这个,在用户名密码错误的时候会弹出Spring Security原生的登录提示框。还有就是,为什么这里没有处理token,因为Ng-Alain的默认的拦截器已经对token进行添加处理。// noinspection SpellCheckingInspection/* * Yunzhi拦截器,用于实现添加url,添加header,全局异常处理 /@Injectable()export class YunzhiInterceptor implements HttpInterceptor { constructor(private router: Router) { } intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { /* * 为request加上服务端前缀 / let url = req.url; if (!url.startsWith(‘https://’) && !url.startsWith(‘http://’)) { url = environment.SERVER_URL + url; } let request = req.clone({ url }); /* * 设置headers,防止弹出对话框 * https://stackoverflow.com/questions/37763186/spring-boot-security-shows-http-basic-auth-popup-after-failed-login / let headers = request.headers; headers = headers.append(‘X-Requested-With’, ‘XMLHttpRequest’); request = request.clone({ headers: headers }); /* * 数据过滤 */ return next.handle(request).pipe( // mergeMap = merge + map mergeMap((event: any) => { return of(event); }), // Observable对象发生错误时,执行catchError catchError((error: HttpErrorResponse) => { return this.handleHttpException(error); }), ); } private handleHttpException(error: HttpErrorResponse): Observable<HttpErrorResponse> { switch (error.status) { case 401: if (this.router.url !== ‘/passport/login’) { // noinspection JSIgnoredPromiseFromCall this.router.navigateByUrl(’/passport/login’); } break; case 403: case 404: case 500: // noinspection JSIgnoredPromiseFromCall this.router.navigateByUrl(/${error.status}); break; } // 最终将异常抛出来,便于组件个性化处理 throw new Error(error.error); }}解决H2控制台看不见问题Spring Security直接把H2数据库的控制台也拦截了,且禁止查看,启用以下配置恢复控制台查看。@Componentpublic class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // 使用Basic认证方式进行验证进行验证 .httpBasic() // 要求SpringSecurity对后台的任何请求进行认证保护 .and().authorizeRequests().anyRequest().authenticated() // 关闭Security的Session,使用Spring Session .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER) // 设置frameOptions为sameOrigin,否则看不见h2控制台 .and().headers().frameOptions().sameOrigin() // 禁用csrf,否则403. 这个在上线的时候判断是否需要开启 .and().csrf().disable(); }}总结一款又一款框架,是前辈们智慧的结晶。永远,文档比书籍更珍贵! ...

February 22, 2019 · 4 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 Role过滤Jackson JSON输出内容

在本文中,我们将展示如何根据Spring Security中定义的用户角色过滤JSON序列化输出。为什么我们需要过滤?让我们考虑一个简单但常见的用例,我们有一个Web应用程序,为不同角色的用户提供服务。例如,这些角色为User和Admin。首先,让我们定义一个要求,即Admin可以完全访问通过公共REST API公开的对象的内部状态。相反,User用户应该只看到一组预定义的对象属性。我们将使用Spring Security框架来防止对Web应用程序资源的未授权访问。让我们定义一个对象,我们将在API中作为REST响应返回数据:class Item { private int id; private String name; private String ownerName; // getters}当然,我们可以为应用程序中的每个角色定义一个单独的数据传输对象类。但是,这种方法会为我们的代码库引入无用的重复或复杂的类层次结构。另一方面,我们可以使用Jackson库的JSON View功能。正如我们将在下一节中看到的那样,它使得自定义JSON表示就像在字段上添加注释一样简单。@JsonView注释Jackson库支持通过使用@JsonView注解标记我们想要包含在JSON表示中的字段来定义多个序列化/反序列化上下文。此注解具有Class类型的必需参数,用于区分上下文。使用@JsonView在我们的类中标记字段时,我们应该记住,默认情况下,序列化上下文包括未明确标记为视图一部分的所有属性。为了覆盖此行为,我们可以禁用DEFAULT_VIEW_INCLUSION映射器功能。首先,让我们定义一个带有一些内部类的View类,我们将它们用作@JsonView注解的参数:class View { public static class User {} public static class Admin extends User {}}接下来,我们将@JsonView注解添加到我们的类中,使ownerName只能访问admin角色:@JsonView(View.User.class)private int id;@JsonView(View.User.class)private String name;@JsonView(View.Admin.class)private String ownerName;如何将@JsonView注解与Spring Security 集成现在,让我们添加一个包含所有角色及其名称的枚举。之后,让我们介绍JSONView和安全角色之间的映射:enum Role { ROLE_USER, ROLE_ADMIN} class View { public static final Map<Role, Class> MAPPING = new HashMap<>(); static { MAPPING.put(Role.ADMIN, Admin.class); MAPPING.put(Role.USER, User.class); } //…}最后,我们来到了整合的中心点。为了绑定JSONView和Spring Security角色,我们需要定义适用于我们应用程序中所有控制器方法的控制器。到目前为止,我们唯一需要做的就是覆盖AbstractMappingJacksonResponseBodyAdvice类的 beforeBodyWriteInternal方法:@RestControllerAdviceclass SecurityJsonViewControllerAdvice extends AbstractMappingJacksonResponseBodyAdvice { @Override protected void beforeBodyWriteInternal( MappingJacksonValue bodyContainer, MediaType contentType, MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) { if (SecurityContextHolder.getContext().getAuthentication() != null && SecurityContextHolder.getContext().getAuthentication().getAuthorities() != null) { Collection<? extends GrantedAuthority> authorities = SecurityContextHolder.getContext().getAuthentication().getAuthorities(); List<Class> jsonViews = authorities.stream() .map(GrantedAuthority::getAuthority) .map(AppConfig.Role::valueOf) .map(View.MAPPING::get) .collect(Collectors.toList()); if (jsonViews.size() == 1) { bodyContainer.setSerializationView(jsonViews.get(0)); return; } throw new IllegalArgumentException(“Ambiguous @JsonView declaration for roles " + authorities.stream() .map(GrantedAuthority::getAuthority).collect(Collectors.joining(”,"))); } }}这样,我们的应用程序的每个响应都将通过这个路由,它将根据我们定义的角色映射找到合适的返回结果。请注意,此方法要求我们在处理具有多个角色的用户时要小心。 ...

February 15, 2019 · 1 min · jiezi

[Spring Security 5.2.0] 8.1.3 Authentication

8.1.3 AuthenticationSpring Security can participate in many different authentication environments. While we recommend people use Spring Security for authentication and not integrate with existing Container Managed Authentication, it is nevertheless supported - as is integrating with your own proprietary authentication system.Spring Security可以参与许多不同的身份验证环境。虽然我们建议人们使用Spring Security进行身份验证,而不是与现有的容器管理身份验证集成,但是它仍然受到支持——就像与您自己的专有身份验证系统集成一样。What is authentication in Spring Security?Let’s consider a standard authentication scenario that everyone is familiar with.1, A user is prompted to log in with a username and password.2, The system (successfully) verifies that the password is correct for the username.3, The context information for that user is obtained (their list of roles and so on).4, A security context is established for the user5, The user proceeds, potentially to perform some operation which is potentially protected by an access control mechanism which checks the required permissions for the operation against the current security context information.让我们考虑一个每个人都熟悉的标准身份验证场景。1, 提示用户使用用户名和密码登录。2, 系统(成功)验证用户名的密码是否正确。3, 获取该用户的上下文信息(角色列表等)。4, 为用户建立一个安全上下文5, 用户继续执行某些操作,这些操作可能受到访问控制机制的保护,该机制根据当前安全上下文信息检查操作所需的权限。The first three items constitute the authentication process so we’ll take a look at how these take place within Spring Security.1, The username and password are obtained and combined into an instance of UsernamePasswordAuthenticationToken (an instance of the Authentication interface, which we saw earlier).2, The token is passed to an instance of AuthenticationManager for validation.3, The AuthenticationManager returns a fully populated Authentication instance on successful authentication.4, The security context is established by calling SecurityContextHolder.getContext().setAuthentication(…), passing in the returned authentication object.前三项构成了身份验证过程,因此我们将了解这些在Spring Security中是如何发生的。1, 用户名和密码被获取并组合到UsernamePasswordAuthenticationToken的实例中(Authenticationinterface的实例,我们在前面看到过)。2, 令牌传递给AuthenticationManager的一个实例进行验证。3, AuthenticationManager在身份验证成功时返回一个完整填充的身份验证实例。4, 安全上下文是通过调用securitycontext.getcontext().setauthentication(…),传入返回的身份验证对象来建立的。From that point on, the user is considered to be authenticated. Let’s look at some code as an example.从那时起,用户被认为是经过身份验证的。让我们以一些代码为例。import org.springframework.security.authentication.;import org.springframework.security.core.;import org.springframework.security.core.authority.SimpleGrantedAuthority;import org.springframework.security.core.context.SecurityContextHolder;public class AuthenticationExample {private static AuthenticationManager am = new SampleAuthenticationManager();public static void main(String[] args) throws Exception { BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); while(true) { System.out.println(“Please enter your username:”); String name = in.readLine(); System.out.println(“Please enter your password:”); String password = in.readLine(); try { Authentication request = new UsernamePasswordAuthenticationToken(name, password); Authentication result = am.authenticate(request); SecurityContextHolder.getContext().setAuthentication(result); break; } catch(AuthenticationException e) { System.out.println(“Authentication failed: " + e.getMessage()); } } System.out.println(“Successfully authenticated. Security context contains: " + SecurityContextHolder.getContext().getAuthentication());}}class SampleAuthenticationManager implements AuthenticationManager {static final List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>();static { AUTHORITIES.add(new SimpleGrantedAuthority(“ROLE_USER”));}public Authentication authenticate(Authentication auth) throws AuthenticationException { if (auth.getName().equals(auth.getCredentials())) { return new UsernamePasswordAuthenticationToken(auth.getName(), auth.getCredentials(), AUTHORITIES); } throw new BadCredentialsException(“Bad Credentials”);}}Here we have written a little program that asks the user to enter a username and password and performs the above sequence. The AuthenticationManager which we’ve implemented here will authenticate any user whose username and password are the same. It assigns a single role to every user. The output from the above will be something like:在这里,我们编写了一个小程序,要求用户输入用户名和密码并执行上面的顺序。我们在这里实现的AuthenticationManager将对用户名和密码相同的任何用户进行身份验证。它为每个用户分配一个角色。上面的输出将类似于:Please enter your username:bobPlease enter your password:passwordAuthentication failed: Bad CredentialsPlease enter your username:bobPlease enter your password:bobSuccessfully authenticated. Security context contains: \org.springframework.security.authentication.UsernamePasswordAuthenticationToken@441d0230: \Principal: bob; Password: [PROTECTED]; \Authenticated: true; Details: null; \Granted Authorities: ROLE_USERNote that you don’t normally need to write any code like this. The process will normally occur internally, in a web authentication filter for example. We’ve just included the code here to show that the question of what actually constitutes authentication in Spring Security has quite a simple answer. A user is authenticated when the SecurityContextHolder contains a fully populated Authentication object.请注意,通常不需要编写这样的代码。这个过程通常发生在内部,例如在web身份验证过滤器中。我们刚刚在这里包含了一些代码,以说明在Spring Security中真正构成身份验证的问题有一个非常简单的答案。当SecurityContextHolder包含一个完全填充的身份验证对象时,对用户进行身份验证。Setting the SecurityContextHolder Contents DirectlyIn fact, Spring Security doesn’t mind how you put the Authentication object inside the SecurityContextHolder. The only critical requirement is that the SecurityContextHolder contains an Authentication which represents a principal before the AbstractSecurityInterceptor (which we’ll see more about later) needs to authorize a user operation.You can (and many users do) write their own filters or MVC controllers to provide interoperability with authentication systems that are not based on Spring Security. For example, you might be using Container-Managed Authentication which makes the current user available from a ThreadLocal or JNDI location. Or you might work for a company that has a legacy proprietary authentication system, which is a corporate “standard” over which you have little control. In situations like this it’s quite easy to get Spring Security to work, and still provide authorization capabilities. All you need to do is write a filter (or equivalent) that reads the third-party user information from a location, build a Spring Security-specific Authentication object, and put it into the SecurityContextHolder. In this case you also need to think about things which are normally taken care of automatically by the built-in authentication infrastructure. For example, you might need to pre-emptively create an HTTP session to cache the context between requests, before you write the response to the client footnote:[It isn’t possible to create a session once the response has been committed.实际上,Spring Security并不介意您如何将身份验证对象放入SecurityContextHolder中。惟一的关键需求是,SecurityContextHolder包含一个身份验证,在AbstractSecurityInterceptor(稍后将详细介绍)需要授权用户操作之前,该身份验证代表一个主体。您可以(许多用户也可以)编写自己的过滤器或MVC控制器,以提供与不基于Spring安全性的身份验证系统的互操作性。例如,您可能正在使用容器管理的身份验证,这使得当前用户可以从ThreadLocal或JNDI位置访问。或者,您可能为一家拥有遗留专有身份验证系统的公司工作,该系统是一个您几乎无法控制的公司“标准”。在这种情况下,很容易让Spring Security工作,并且仍然提供授权功能。您所需要做的就是编写一个过滤器(或等效的过滤器),从一个位置读取第三方用户信息,构建一个Spring特定于安全的身份验证对象,并将其放入SecurityContextHolder中。在这种情况下,您还需要考虑通常由内置身份验证基础设施自动处理的事情。例如,您可能需要先创建一个HTTP会话,以便在请求之间缓存上下文,然后再编写对客户机脚注的响应:[不可能在提交响应之后创建会话。If you’re wondering how the AuthenticationManager is implemented in a real world example, we’ll look at that in the core services chapter.如果您想知道AuthenticationManager在实际示例中是如何实现的,我们将在核心服务一章中对此进行介绍。 ...

February 14, 2019 · 4 min · jiezi

[Spring Security Reference 5.2.0 翻译] 8

Architecture and ImplementationOnce you are familiar with setting up and running some namespace-configuration based applications, you may wish to develop more of an understanding of how the framework actually works behind the namespace facade. Like most software, Spring Security has certain central interfaces, classes and conceptual abstractions that are commonly used throughout the framework. In this part of the reference guide we will look at some of these and see how they work together to support authentication and access-control within Spring Security.8.1 Technical Overview ...

February 14, 2019 · 1 min · jiezi

springSecurity 登录以及用户账号密码解析原理

springSecurity 拦截器链用户登录基本流程处理如下:1 SecurityContextPersistenceFilter 2 AbstractAuthenticationProcessingFilter3 UsernamePasswordAuthenticationFilter4 AuthenticationManager5 AuthenticationProvider6 userDetailsService7 userDetails8 认证通过9 SecurityContext10 SecurityContextHolder11 AuthenticationSuccessHandler用户页面登录1 首先进入 SecurityContextPersistenceFilter 拦截器public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { 。。。。。省略 //HttpRequestResponseHolder 对象 HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response); // 判断session是否存在,如果不存在则新建一个session SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder); boolean var13 = false; try { var13 = true; //将securiryContext放入SecurityContextHolder中 SecurityContextHolder.setContext(contextBeforeChainExecution); //调用下一个拦截器,也就是之后所有的拦截器 chain.doFilter(holder.getRequest(), holder.getResponse()); var13 = false; } finally { if (var13) { //获取从context SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext(); //清空SecurityContextHolder 中的contex 临时保存 SecurityContextHolder.clearContext(); //保存后面过滤器生成的数据 到SecurityContextRepository中 this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse()); request.removeAttribute("__spring_security_scpf_applied"); if (debug) { this.logger.debug(“SecurityContextHolder now cleared, as request processing completed”); } } } //从SecurityContextHolder获取SecurityContext实例 8 SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext(); //清空SecurityContextHolder中的SecurityContext SecurityContextHolder.clearContext(); //将SecurityContext实例保存到session中,以便下次请求时候用 9 this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse()); request.removeAttribute("__spring_security_scpf_applied"); if (debug) { this.logger.debug(“SecurityContextHolder now cleared, as request processing completed”); } }}loadContext 判断session是否存在 没有新建一个public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) { HttpServletRequest request = requestResponseHolder.getRequest(); HttpServletResponse response = requestResponseHolder.getResponse(); //如果session不存在则返回null HttpSession httpSession = request.getSession(false); //根据 private String springSecurityContextKey = “SPRING_SECURITY_CONTEXT”; //获取原来的session SecurityContext context = this.readSecurityContextFromSession(httpSession); if (context == null) { if (this.logger.isDebugEnabled()) { this.logger.debug(“No SecurityContext was available from the HttpSession: " + httpSession + “. A new one will be created.”); } //如果session 为null 则新建一个 context = this.generateNewContext(); } //将session 保存到 内部类SaveToSessionResponseWrapper 中 HttpSessionSecurityContextRepository.SaveToSessionResponseWrapper wrappedResponse = new HttpSessionSecurityContextRepository.SaveToSessionResponseWrapper(response, request, httpSession != null, context); //保存在 HttpRequestResponseHolder对象中 requestResponseHolder.setResponse(wrappedResponse); if (this.isServlet3) { requestResponseHolder.setRequest(new HttpSessionSecurityContextRepository.Servlet3SaveToSessionRequestWrapper(request, wrappedResponse)); } return context;}readSecurityContextFromSession 方法中 根据判断session是否存在 会根据 “ SPRING_SECURITY_CONTEXT ” 获取session用户登录即是认证登录的实现方式:2 进入AbstractAuthenticationProcessingFilter类3 UsernamePasswordAuthenticationFilter 实现了类 AbstractAuthenticationProcessingFilter 调用自身的attemptAuthentication方法获取用户名和密码,构建tokenpublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (this.postOnly && !request.getMethod().equals(“POST”)) { throw new AuthenticationServiceException(“Authentication method not supported: " + request.getMethod()); } else { String username = this.obtainUsername(request); String password = this.obtainPassword(request); if (username == null) { username = “”; } if (password == null) { password = “”; } username = username.trim(); //构建token ,此时没有进行验证 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); this.setDetails(request, authRequest); // AuthenticationManager 将token传递给 的 authenticate方法 进行 token验证 return this.getAuthenticationManager().authenticate(authRequest); }}//获取密码protected String obtainPassword(HttpServletRequest request) { return request.getParameter(this.passwordParameter);}//获取账号protected String obtainUsername(HttpServletRequest request) { return request.getParameter(this.usernameParameter);}调用authenticate方法,进行token4 AuthenticationManager 接口1 AuthenticationManager 接口public interface AuthenticationManager { Authentication authenticate(Authentication var1) throws AuthenticationException;}2 ProviderManager 实现了AuthenticationManager 接口 authenticate方法中 result = provider.authenticate(authentication); //返回成功的认证重写authenticate方法 来获取用户验证信息5 AuthenticationProvider 接口中方法Authentication authenticate(Authentication authentication) throws AuthenticationException;AbstractUserDetailsAuthenticationProvider 实现了AuthenticationProvider 接口public Authentication authenticate(Authentication authentication) //authentication 传递过来token throws AuthenticationException { Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> messages.getMessage( “AbstractUserDetailsAuthenticationProvider.onlySupports”, “Only UsernamePasswordAuthenticationToken is supported”)); // Determine username 获取密码 String username = (authentication.getPrincipal() == null) ? “NONE_PROVIDED” : authentication.getName(); boolean cacheWasUsed = true; //从缓存中获取 UserDetails user = this.userCache.getUserFromCache(username); //没有缓存 if (user == null) { cacheWasUsed = false; try { //查询数据库 获取用户账号密码 user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException notFound) { logger.debug(“User ‘” + username + “’ not found”);//此处 不能抛出异常 UsernameNotFoundException if (hideUserNotFoundExceptions) { throw new BadCredentialsException(messages.getMessage( “AbstractUserDetailsAuthenticationProvider.badCredentials”, “Bad credentials”)); } else { throw notFound; } } Assert.notNull(user, “retrieveUser returned null - a violation of the interface contract”); } try { //检查账号是否过期等操作 preAuthenticationChecks.check(user); // additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } catch (AuthenticationException exception) { if (cacheWasUsed) { // There was a problem, so try again after checking // we’re using latest data (i.e. not from the cache) cacheWasUsed = false; user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); preAuthenticationChecks.check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } else { throw exception; } } postAuthenticationChecks.check(user); if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (forcePrincipalAsString) { principalToReturn = user.getUsername(); } //创建成功的认证Authentication return createSuccessAuthentication(principalToReturn, authentication, user);}retrieveUser 方法查询用户数据:调用自身的抽象方法protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException;DaoAuthenticationProvider 实现类DaoAuthenticationProvider 继承 AbstractUserDetailsAuthenticationProvider 重写 retrieveUser 方法用来根据用户名查询用户数据protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { prepareTimingAttackProtection(); try { // 根据用户名查询数据 UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); if (loadedUser == null) { throw new InternalAuthenticationServiceException( “UserDetailsService returned null, which is an interface contract violation”); } return loadedUser; } catch (UsernameNotFoundException ex) { mitigateAgainstTimingAttack(authentication); throw ex; } catch (InternalAuthenticationServiceException ex) { throw ex; } catch (Exception ex) { throw new InternalAuthenticationServiceException(ex.getMessage(), ex); }}6 UserServiceDatils 接口 调用 loadUserByUsername 查询数据7 userDetails对象返回数据UserServiceDatils 接口需要自己实现(6.7一起进行)public class MUserDetailsService implements UserDetailsService { Logger logger = LoggerFactory.getLogger(getClass()); @Autowired private UsersMapper usersMapper; /** *@description: 需要从数据库中通过用户名来查询用户的信息和用户所属的角色 *@author: wangl *@time: 2019/1/8 10:44 @version 1.0 / @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { logger.info("===========授权============”); UserDetails userDeatils = null; //通过用户名 查询密码 Users users = usersMapper.selectUserByUserName(username); Set<MGrantedAuthority> authorities = new HashSet<>(); //查询用户角色 if(null != users){ logger.info(“用户 = " + users.getUsername() + “—-” + users.getPassword()); //查询用户权限 List<Users> usersList = usersMapper.selectRolesAndResourceByUserId(users.getId()); if(null != usersList && usersList.size()>0){ usersList.forEach(user->{ //存放role name 或者 权限名字 authorities.add(new MGrantedAuthority(user.getResourceName())); }); }else{ System.out.println(“用户无权限。。”); throw new BadCredentialsException(“not found … “); }7 返回数据 userDeatils = new Users(users.getUsername(),users.getPassword(),authorities); }else{ throw new BadCredentialsException(“用户名不存在”); } return userDeatils; }查询用户数据之后返回查询到的loadUser对象然后用户账号是否被锁定,过期等验证this.preAuthenticationChecks.check(user);密码验证protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { if (authentication.getCredentials() == null) { this.logger.debug(“Authentication failed: no credentials provided”); throw new BadCredentialsException(this.messages.getMessage(“AbstractUserDetailsAuthenticationProvider.badCredentials”, “Bad credentials”)); } else { //获取密码 String presentedPassword = authentication.getCredentials().toString(); //匹配密码 if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { this.logger.debug(“Authentication failed: password does not match stored value”); throw new BadCredentialsException(this.messages.getMessage(“AbstractUserDetailsAuthenticationProvider.badCredentials”, “Bad credentials”)); } }}自定义密码验证器@Slf4j@Componentpublic class PasswordEncorder implements PasswordEncoder { @Override public String encode(CharSequence charSequence) { log.info("============ charSequence.toString() ============== " + charSequence.toString()); return MD5Util.encode(charSequence.toString()); } @Override public boolean matches(CharSequence charSequence, String s) { String pwd = charSequence.toString(); log.info(“前端传过来密码为: " + pwd); log.info(“加密后密码为: " + MD5Util.encode(charSequence.toString())); //s 应在数据库中加密 if( MD5Util.encode(charSequence.toString()).equals(MD5Util.encode(s))){ return true; } throw new DisabledException(”–密码错误–”); } }11 AuthenticationSuccessHandler/ *@description: 自定义登陆成功处理类 *@author: wangl *@time: 2019/1/14 17:46 @version 1.0 /@Componentpublic class MyAuthenctiationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { logger.info(“登陆成功。。”); String name = authentication.getName(); HttpSession session = request.getSession(); session.setAttribute(“user”,name); response.sendRedirect("/success”); //super.onAuthenticationSuccess(request, response, authentication); }}配置类@Configuration@EnableWebSecuritypublic class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired MUserDetailsService mUserDetailsService; //userDetails @Autowired MyFilterSecurityInterceptor myFilterSecurityInterceptor; //自定义拦截器 @Autowired PasswordEncoder passwordEncoder; //密码验证器 @Autowired private MyAuthenctiationFailureHandler myAuthenctiationFailureHandler;//失败处理 @Autowired private MyAuthenctiationSuccessHandler myAuthenctiationSuccessHandler;//成功处理 @Override public void configure(WebSecurity web) throws Exception { web.ignoring().mvcMatchers("/static/**”); //过滤静态资源 } /定义认证用户信息获取来源,密码校验规则等/ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //从内存中获取 //明文方式提交 / auth.inMemoryAuthentication().withUser(“zs”).password(“1234”).roles(“USER”) .and().withUser(“admin”).password(“1234”).roles(“ADMIN”); //从内存中获取 / auth.userDetailsService(mUserDetailsService) .passwordEncoder(passwordEncoder); //密码加密方式 /auth.authenticationProvider(customAuthenticationProvider) .authenticationProvider(authenticationProvider()) //增加 .userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder());/ / auth.inMemoryAuthentication().passwordEncoder(new PasswordEncorder()).withUser(“zs”).password(“1234”).roles(“USER”) .and().withUser(“admin”).password(“1234”).roles(“ADMIN”) ;/ } /定义安全策略/ @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() ////配置安全策略 .antMatchers(”/”,"/login.html","/loginPage").permitAll() // 定义请求不需要验证 //.antMatchers("/admin/next").hasRole(“ADMIN”) // 设置只有管理员才能访问的url //.antMatchers("/admin/**").hasAnyRole(“ADMIN”) // 设置多个角色访问的url .anyRequest().authenticated() //其余所有请求都需要验证 .and() .formLogin() //配置登录页面 .loginPage("/loginPage") //登录页面访问路径 .loginProcessingUrl("/login") //登录页面提交表单路径 .successHandler(myAuthenctiationSuccessHandler)//成功页面 .failureHandler(myAuthenctiationFailureHandler) //失败后跳转路径 .and() .logout() //登出不需要验证 .logoutUrl("/logout").permitAll() // .and() //.authorizeRequests() //.antMatchers("/admin/next").hasRole(“ADMIN”) //.and() //.rememberMe() //记住我功能 //.tokenValiditySeconds(10000); //自定义的拦截器 , 在适当的地方加入 http.addFilterAt(myFilterSecurityInterceptor, FilterSecurityInterceptor.class); http.csrf().disable(); ///关闭默认的csrf认证 }} ...

January 20, 2019 · 5 min · jiezi

springSecurity 中不能抛出异常UserNameNotFoundException 解析

通过查看源码可得知:1. 前言抽象类中AbstractUserDetailsAuthenticationProvider 接口抛出异常AuthenticationException下面源码注释这么描述 * * @throws AuthenticationException if the credentials could not be validated * (generally a <code>BadCredentialsException</code>, an * <code>AuthenticationServiceException</code> or * <code>UsernameNotFoundException</code>) */ protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException;AuthenticationException 抛出的情况是 BadCredentialsException,AuthenticationServiceException,UsernameNotFoundException这三个异常。当UserNameNotFoundException 这个异常的情况下会抛出可实际情况下我们 查询的user为null 抛出了 UserNameNotFoundException 这个异常但是实际并没有抛出来,抛出的是 AuthenticationException 通过继续往下查看源码后明白了,原来是做了对UserNameNotFoundException 处理,转换成了AuthenticationException 这个异常;hideUserNotFoundExceptions = true;…boolean cacheWasUsed = true; UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException notFound) { logger.debug(“User ‘” + username + “’ not found”); if (hideUserNotFoundExceptions) { throw new BadCredentialsException(messages.getMessage( “AbstractUserDetailsAuthenticationProvider.badCredentials”, “Bad credentials”)); } else { throw notFound; } }所以我们没有抛出UsernameNotFoundException 这个异常,而是将这个异常进行了转换。2.解决办法如何抛出这个异常,那就是将hideUserNotFoundExceptions 设置为 false;2.1设置实现类中 @Bean public DaoAuthenticationProvider authenticationProvider() { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setHideUserNotFoundExceptions(false); provider.setUserDetailsService(mUserDetailsService); provider.setPasswordEncoder(passwordEncoder); return provider; }最后在WebSecurityConfig配置即可 auth.authenticationProvider(daoAuthenticationProvider());2.2 debug来看一下设置之前设置之后抛出的UsernameNotFoundException 异常已经捕获到了,然后进入if中最后抛出new BadCredentialsException(messages.getMessage( “AbstractUserDetailsAuthenticationProvider.badCredentials”, “Bad credentials”))会将异常信息存入session中 , 根据key即可获取最后在失败的处理器中获取到@Componentpublic class MyAuthenctiationFailureHandler extends SimpleUrlAuthenticationFailureHandler { public MyAuthenctiationFailureHandler() { this.setDefaultFailureUrl("/loginPage"); } @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { logger.info(“进入认证失败处理类”); HttpSession session = request.getSession(); AuthenticationException attributes = (AuthenticationException) session.getAttribute(“SPRING_SECURITY_LAST_EXCEPTION”); logger.info(“aaaa " + attributes); super.onAuthenticationFailure(request, response, exception); }}这样子做可以直接在session中获取到,如果自定义抛出一个异常首先控制台会报异常错,其次前台的通过如ajax获取错误信息,又得写ajax。这样子做直接将信息存入session中,springSecurity直接为我们封装到session中了,可以直接根据key获取到。如: ${session.SPRING_SECURITY_LAST_EXCEPTION.message}2.4 判断密码注意 如果用户名不存在,抛了异常不要再在密码验证其中抛出密码错误异常,不然抛出UserNameNotFoundException 后还会验证密码是否正确,如果密码正确还好,返回true,如果不正确抛出异常。此时会将 UsernameNotFoundException 异常覆盖,这里应该返回false。源码如下:protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { prepareTimingAttackProtection(); try { UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); if (loadedUser == null) { throw new InternalAuthenticationServiceException( “UserDetailsService returned null, which is an interface contract violation”); } return loadedUser; } catch (UsernameNotFoundException ex) { // 、、、、、、、 这里会去匹配密码是否正确 mitigateAgainstTimingAttack(authentication); throw ex; } catch (InternalAuthenticationServiceException ex) { throw ex; } catch (Exception ex) { throw new InternalAuthenticationServiceException(ex.getMessage(), ex); } }mitigateAgainstTimingAttack 方法 private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) { if (authentication.getCredentials() != null) { String presentedPassword = authentication.getCredentials().toString(); //这里会是自定义密码加密 this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword); } }我的密码加密器 @Override public boolean matches(CharSequence charSequence, String s) { String pwd = charSequence.toString(); log.info(“前端传过来密码为: " + pwd); log.info(“加密后密码为: " + MD5Util.encode(charSequence.toString())); log.info(“数据库存储的密码: " + s); //s 应在数据库中加密 if( MD5Util.encode(charSequence.toString()).equals(MD5Util.encode(s))){ return true; } //throw new DisabledException(”–密码错误–”); //不能抛出异常 return false; }如下是 我们密码验证器里抛出异常后获取到的异常异常密码未验证 之前捕获到的异常信息验证密码后捕获到的异常 (这里跳到ProviderManager中)既然我用户名不对我就不必要验证密码了,所以不应该抛出异常,应该直接返回false。不然。此处密码异常会将 用户不存在进行覆盖!3. 验证页面代码<body>登录页面<div th:text="${session.SPRING_SECURITY_LAST_EXCEPTION!=null?session.SPRING_SECURITY_LAST_EXCEPTION.message:’’}">[…]</div><form method=“post” action="/login” > <input type=“text” name=“username” /><br> <input type=“password” name=“password” /> <input type=“submit” value=“login” /></form> ...

January 20, 2019 · 2 min · jiezi