深圳网站建设怎么办,公司网站建设申请书,如何编辑网站源代码,软件开发联系电话目录
关于Spring Security框架
Spring Security框架的依赖项
Spring Security框架的典型特征 关于Spring Security的配置
关于默认的登录页
关于请求的授权访问#xff08;访问控制#xff09; 使用自定义的账号登录
使用数据库中的账号登录
关于密码编码器
使用BCry…目录
关于Spring Security框架
Spring Security框架的依赖项
Spring Security框架的典型特征 关于Spring Security的配置
关于默认的登录页
关于请求的授权访问访问控制 使用自定义的账号登录
使用数据库中的账号登录
关于密码编码器
使用BCrypt算法
关于伪造的跨域攻击
使用前后端分离的登录
关于认证的标准
未通过认证时拒绝访问
识别当事人Principal
实现根据权限限制访问
补充解释关于使用resultMap标签
基于方法的权限检查
添加Token
首先添加Token-JWT的依赖项
生成JWT 解析JWT
补充
在项目中使用JWT识别用户的身份
核心流程
验证登录成功时响应JWT
解析客户端携带的JWT 我们这次选择去继承Spring系列框架提供的OncePerRequestFilter这个类。
关于认证信息中的当事人
处理解析JWT时的异常
处理复杂请求的跨域问题
单点登录 关于Spring Security框架
Spring Security框架主要解决了认证与授权相关的问题。
认证信息Authentication表示用户的身份信息
认证Authenticate识别用户的身份信息的行为例如登录
授权Authorize授予用户权限使之可以进行某些访问反之如果用户没有得到必要的授权将无法进行访问 Spring Security框架的依赖项
在Spring Boot中使用Spring Security时需要添加spring-boot-starter-security依赖。 Spring Security框架的典型特征 当添加了spring-boot-starter-security依赖后在启动项目时执行一些自动配置具体表现有 所有请求包括根本不存在的都是必须要登录才允许访问的如果未登录会自动跳转到框架自带的登录页面1.项目重启之后需要重新登录2.原来想去的页面会要求登录登录完成之后回到原来的位置 当尝试登录时如果在打开登录页面后重启过服务器端则第1次的输入是无效的 默认的用户名是user密码是在启动项目是控制台提示的一段UUID值每次启动项目时都不同(同一时空的唯一性即同一时间同一空间的值都不同) UUID是通过128位算法运算结果是128个bit运算得到的是一个随机数在同一时空是唯一的通常使用32个十六进制数来表示每种平台生成UUID的API和表现可能不同UUID值的种类有2的128次方个即3.4028237e38也就是340282366920938463463374607431768211456 当登录成功后会自动跳转到此前尝试访问的URL 当登录成功后可以通过 /logout 退出登录 默认不接受普通POST请求如果提交POST请求将响应403Forbidden 关于Spring Security的配置 在项目的根包下创建config.SecurityConfiguration类作为Spring Security的配置类此类需要继承自WebSecurityConfigurerAdapter并重写void configure(HttpSecurity http)方法例如
Slf4j
Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {Overrideprotected void configure(HttpSecurity http) throws Exception {// super.configure(http); // 不要保留调用父级同名方法的代码不要保留不要保留不要保留}}
做了配置后此时重启工程就不需要登陆了就算访问登录页面也没有。 写此配置是为了调整Spring Security框架的特征的所有表现由自己来设置。 关于默认的登录页
在自定义的配置类中的void configure(HttpSecurity http)方法中调用参数对象的formLogin()方法即可开启默认的登录表单如果没有调用此方法则不会应用默认的登录表单例如
Slf4j
Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {Overrideprotected void configure(HttpSecurity http) throws Exception {// super.configure(http); // 不要保留调用父级同名方法的代码不要保留不要保留不要保留// 如果调用以下方法当Security认为需要通过认证但实际未通过认证时就会跳转到登录页面// 如果未调用以下方法将会响应403错误http.formLogin();}} 关于请求的授权访问访问控制
在刚刚添加spring-boot-starter-security时所有请求都是需要登录后才允许访问的当添加了自定义的配置类且没有调用父级同名方法后所有请求都是不需要登录就可以访问的
为了实现一部分需要登录一部分不需要登录就需要做配置类不然如果是自己做了一个登录页面访问登录页面还需要登录就不合适。
在配置类中的void configure(HttpSecurity http)方法中调用参数对象的authorizeRequests()方法开始配置授权访问
Override
protected void configure(HttpSecurity http) throws Exception {// 白名单// 使用1个星号可以通配此层级的任何资源例如/admin/*可以匹配/admin/add-new、/admin/list但不可以匹配/admin/password/change// 使用2个连续的星可以可以通配若干层级的资源例如/admin/**可以匹配/admin/add-new、/admin/password/changeString[] urls {/doc.html,/**/*.css,/**/*.js,/swagger-resources,/v2/api-docs,};// 配置授权访问// 注意以下授权访问的配置是遵循“第一匹配原则”的即“以最先匹配到的规则为准”// 例如anyRequest()是匹配任何请求通常应该配置在最后表示“除了以上配置过的以外的所有请求”// 所以在开发实践中应该将更具体的请求配置在靠前的位置将更笼统的请求配置在靠后的位置http.authorizeRequests() // 开始对请求进行授权.mvcMatchers(urls) // 匹配某些请求.permitAll() // 许可即不需要通过认证就可以访问.anyRequest() // 任何请求.authenticated() // 要求已经完成认证的;
} http.authorizeRequests() // 开始对请求进行授权
表示开始对请求进行授权 。 .anyRequest() // 任何请求 .authenticated() // 要求已经完成认证的
上面两句需要连起来理解表示任何请求都要求是已经完成认证的。加上这两句就回到了最开始的样子所有的请求都需要登录才能访问不登录访问不了。 .mvcMatchers(urls) // 匹配某些请求 .permitAll() // 许可即不需要通过认证就可以访问
这两句话也是连起来理解的 理解同上面一样上面是任何请求这里是匹配某些请求上面的行为是所有都要求认证这里的行为是许可访问不需要通过认证。因为遵循“第一匹配原则”的即“以最先匹配到的规则为准”所有这两行代码要放在最上面才有效 这里的urls是怎么来的 首先为了方便页面正确显示勾上禁用缓存。 看下面错误提示看到有一堆的200都是login的为了更好的看到提示信息把http.formLogin关掉。 可以看到大量的403 从中我们对这些403进行许可(案例访问的API文档给文档需要的资源进行许可就可以顺利访问API文档了urls就是这么来的。 注意有的比如表面是说的api-docs这样一个名字实际在配白名单的时候看它的url是在一个v2的文件夹里面配置为 /v2/api-docs。 使用自定义的账号登录
在使用Spring Security框架时可以自定义组件类实现UserDetailsService接口则Spring Security就会基于此类的对象来处理认证
则在项目的根包下创建security.UserDetailsServiceImpl在类上添加Service注解使其成为组件类实现UserDetailsService接口
Slf4j
Service
public class UserDetailsServiceImpl implements UserDetailsService {Overridepublic UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {return null;}
}
通过loadUserByUsername(String s)这个方法的名字可以理解为通过用户名加载用户参数s就是username用户名返回UserDetails用户详情
在项目中存在UserDetailsService接口类型的组件对象时尝试登录时Spring Security就会自动使用登录表单中输入的用户名来调用以上方法 把输入的用户名作为一个参数 并得到方法返回的UserDetails类型的结果 此结果中应该包含用户的相关信息例如密码、账号状态、权限等等接下来Spring Security框架会自动判断账号的状态例如是否启用或禁用、验证密码在UserDetails中的密码与登录表单中的密码是否匹配等从而决定此次是否登录成功 所以对于开发者而言在以上方法中只需要完成“根据用户名返回匹配的用户详情”即可例如
Slf4j
Service
public class UserDetailsServiceImpl implements UserDetailsService {Overridepublic UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {log.debug(用户名{}, s);// 假设正确的用户名是root匹配的密码是1234if (!root.equals(s)) {log.debug(此用户名没有匹配的用户数据将返回null);return null;}log.debug(用户名匹配成功准备返回此用户名匹配的UserDetails类型的对象);UserDetails userDetails User.builder().username(s).password(1234).disabled(false) // 账号状态是否禁用.accountLocked(false) // 账号状态是否锁定.accountExpired(false) // 账号状态是否过期.credentialsExpired(false) // 账号的凭证是否过期.authorities(这是一个临时使用的山寨的权限) // 权限.build();log.debug(即将向Spring Security返回UserDetails类型的对象{}, userDetails);return userDetails;}} 以上代码中用User.builder()开启它的构建者模式 .build()表示构建完了。这是一个链式写法先有个builder()在执行 .build()就可以创建这个对象。创建的过程中就传入例如密码、账号状态、权限等相关信息。
当项目中存在UserDetailsService类型的对象后启动项目时控制台不会再提示临时使用的UUID密码并且user账号也不可用 用的就是自己配的 .username(s) .password(1234)这个。 另外Spring Security框架认为所有的密码都是必须显式的经过某种算法处理过的如果使用的密码是明文原始密码例如1234这种也必须明确的指出例如使用没加密的原始密码在Security的配置类中添加配置NoOpPasswordEncoder这种密码编码器告诉Security是没有加密的不然会报错
Bean
public PasswordEncoder passwordEncoder() {return NoOpPasswordEncoder.getInstance();
} 此时尝试登录输入用户名root密码1234登录成功输入错误的用户名提示以下为null这是因为这个null是在用户名不对的时候我们给它的。 如果用户名是输入的root密码故意输出会提示 如果把禁用打开输入正确的用户名密码也会显示用户已失效 使用数据库中的账号登录
需要将UserDetailsServiceImpl中的实现改为“根据用户名查询数据库中的用户信息”需要执行的SQL语句大致是
select username, password, enable from ams_admin where username? 在pojo.vo.AdminLoginInfoVO类
Data
Accessors(chain true)
public class AdminLoginInfoVO implements Serializable {private String username;private String password;private Integer enable;
} 在AdminMapper接口中添加抽象方法
AdminLoginInfoVO getLoginInfoByUsername(String username); 在AdminMapper.xml中配置SQL
!-- AdminLoginInfoVO getLoginInfoByUsername(String username); --
select idgetLoginInfoByUsername resultTypecn.tedu.csmall.passport.pojo.vo.AdminLoginInfoVOSELECT username, password, enable FROM ams_admin WHERE username#{username}
/select 在AdminMapperTests中编写并执行测试
Test
void getStandardById() {String username root;Object queryResult mapper.getLoginInfoByUsername(username);System.out.println(根据【username username 】查询数据完成结果 queryResult);
} 然后在UserDetailsServiceImpl中调整原来的实现改成 Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {log.debug(Spring Security框架自动调用了UserDetailsServiceImpl.loadUserByUsername()方法用户名{}, s);// 根据用户名从数据库中查询匹配的用户信息AdminLoginInfoVO loginInfo adminMapper.getLoginInfoByUsername(s);if (loginInfo null) {log.debug(此用户名没有匹配的用户数据将返回null);return null;}log.debug(用户名匹配成功准备返回此用户名匹配的UserDetails类型的对象);UserDetails userDetails User.builder().username(loginInfo.getUsername()).password(loginInfo.getPassword()).disabled(loginInfo.getEnable() 0) // 账号状态是否禁用.accountLocked(false) // 账号状态是否锁定.accountExpired(false) // 账号状态是否过期.credentialsExpired(false) // 账号的凭证是否过期.authorities(这是一个临时使用的山寨的权限) // 权限.build();log.debug(即将向Spring Security返回UserDetails类型的对象{}, userDetails);return userDetails;
}
为了得到较好的运行效果应该在数据表中插入一些新的测试数据例如 因为目前配置的密码编码器是NoOpPasswordEncoder所以本次测试运行时使用的账号在数据库的密码应该是明文密码
关于密码编码器 Spring Security定义了PasswordEncoder接口可以有多种不同的实现此接口中的抽象方法主要有
// 对原密码进行编码返回编码后的结果密文
String encode(String rawPassword);// 验证密码原文第1个参数和密文第2个参数是否匹配
boolean matches(String rawPassword, String encodedPassword);
常见的对密码进行编码实现“加密”效果所使用的算法主要有 MDMessage Digest系列MD2 / MD4 / MD5 SHASecure Hash Algorithm系列SHA-1 / SHA-256 / SHA-384 / SHA-512 BCrypt SCrypt
目前推荐使用的算法是BCrypt算法在Spring Security框架中也提供了BCryptPasswordEncoder类其基本使用
public class BCryptTests {PasswordEncoder passwordEncoder new BCryptPasswordEncoder();Testvoid encode() {String rawPassword 123456;System.out.println(原文 rawPassword);for (int i 0; i 5; i) {String encodedPassword passwordEncoder.encode(rawPassword);System.out.println(密文 encodedPassword);}}// 原文123456// 密文$2a$10$YOW67gn1jGQsNd1lWFOktuxGEK3Ai4obSCo6m0o0zP3YA4iTm0QoS// 密文$2a$10$AoGlKthb1ZKzTAng5ssX6OUwN8.tC9junqbYhtF0POkr.XdFuoEWy// 密文$2a$10$wgBhSmnoFQ.LdvFCLd8lyOSsHuGVIpVYKW8.bW4yt2kBMYqG1G.5u// 密文$2a$10$OIiWGSjFH02Vr9khLEQnG.s2rGowkotMV14TThAgJK8KQm.WQq6pm// 密文$2a$10$DluGioTO7Zcc0hmwDz8Ld.4Uyp2hIIZ/PcGhFCVd1P3FuSukqJN36Testvoid matches() {String rawPassword 123456;System.out.println(原文 rawPassword);String encodedPassword $2a$10$wgBhSmnoFQ.LdvFCLd8lyOSsHuGVIpVYKW8.bW4yt2kBMYqG1G.5u;System.out.println(密文 encodedPassword);boolean result passwordEncoder.matches(rawPassword, encodedPassword);System.out.println(匹配结果 result);}}
关于BCrypt算法其典型特征有 使用同样的原文每次得到的密文都不相同 BCrypt算法在编码过程中使用了随机的“盐”salt值所以每次编码结果都不同 编码结果中保存了这个随机的盐值所以并不影响验证是否匹配 运算效率极为低下可以非常有效的避免暴力破解 可以通过构造方法传入strength值增加强度默认为10表示运算过程中执行2的多少次方的哈希运算 此特征是MD系列和SHA家庭的算法所不具备的特征
另外SCrypt算法的安全性比BCrypt还要高但是执行效率比BCrypt更低通常由于BCrypt算法已经能够提供足够的安全强度所以目前使用BCrypt是常见的选择。 使用BCrypt算法
只需要在Security配置类中将密码编码器换成BCryptPasswordEncoder即可 接下来便可以使用数据库中那些密码是密文的账号测试登录 (注意:专业名词上BCrypt算法以及上文提到的MD等都不是加密算法加密算法是能加密还能解密的而这些算法都是单向加密不可逆和还原的。登录的时候只能用数据库的密文和传递进来的密文做匹配是不能验证原密码的。) 在以上案例中还不能使用post请求需要以下 Spring Security框架设计了“防止伪造的跨域攻击”的防御机制所以默认情况下自定义的POST请求是不可用的简单的解决方案就是在Spring Security的配置类中禁用这个防御机制即可例如 关于伪造的跨域攻击 伪造的跨域攻击此类攻击原理是利用服务器端对客户端浏览器的“信任”来实现的目前主流的浏览器都是多选项卡模式的假设在第1个选项卡中登录了某个网站在第2个选项卡也打开这个网站的页面就会被当作是已经登录的状态基于这种特征假设在第1个选项卡中登录了某个网上银行在第2个选项卡中打开了某个坏人的网站不是网上银行的网站但是在这个坏人的网站的页面中隐藏了一个使用网上银行进行转账的请求这个请求在坏人的网站的页面刚刚打开时就自动发送出去了自动发送方法很多例如将URL设置为某个不显示的img标签的src值由于在第1个选项卡中已经登录了网上银行从第2个选项卡中发出的请求也会被视为已经登录网上银行的状态这就实现了一种攻击行为当然以上只是举例真正的银行转账不会这么简单例如还需要输入密码、手机验证码等等但是这种模式的攻击行为是确实存在的由于使用另一个网站坏人的网站偷偷的实现的攻击所以称之为“伪造的跨域攻击” 典型的防御手段在Spring Security框架中默认就开启了对于“伪造跨域攻击”的防御机制其做法是在所有POST表单中隐藏一个具有“唯一性”的“随机值”例如UUID值当客户端提交请求时必须提交这个UUID值如果未提交则服务器端将其直接视为攻击行为将拒绝处理此请求以Spring Security默认的登录表单为例 当把防御机制禁用后这个数值也就没有了。 提示此前“如果在打开登录页面后重启过服务器端则第1次的输入是无效的”也是因为这种防御机制当打开登录页服务器端生成了此次使用的UUID但重启服务器后服务器不再识别此前生成的UUID所以第1次的输入是无效的 目前以上已经实现Spring Security框架它默认带来的效果解决了认证和授权的问题最主要的用它来处理登录。但目前还不够还需要实现前后端分离的登录。 使用前后端分离的登录 Spring Security框架自带了登录页面和退出登录页面不是前后端分离的则不可以与自行开发的前端项目中的登录页面进行交互如果要改为前后端分离的模式需要 不再启用服务器端Spring Security框架自带的登录页面和退出登录页面 在配置类中不再调用http.formLogin()即可 使用控制器接收客户端的登录请求 自定义Param类封装客户端将提交的用户名和密码在控制器类中添加接收登录请求的方法 注意需要将此请求配置在“白名单”中不能登录之后在登录 使用Service处理登录的业务 在接口中声明抽象方法并在实现类中重写此方法 具体的验证登录仍可以由Spring Security框架来完成调用AuthenticationManager认证管理器对象的authenticate()方法即可则Spring Security框架会自动基于调用方法时传入的用户名来调用UserDetailsService接口对象的loadUserByUsername()方法并得到返回的UserDetails对象然后自动判断账号状态、对比密码等等 可以在Spring Security的配置类中重写authenticationManagerBean()方法并在此方法上添加Bean注解则可以在任何所需要的位置自动装配AuthenticationManager类型的数据注意不要使用authenticationManager()方法此方法在某些场景例如某些测试等中可能导致死循环最终内存溢出 调用AuthenticationManager认证管理器对象的authenticate()方法传入authentication这个参数。 点开Authentication 发现也是一个接口 而Authentication实现类是 它需要传入参数 有两套构造方法第一套第一个参数是用户名第二个是密码。通过传入的参数取出用户名和密码。 以上根据取出的用户名和密码创建了用户认证对象authentication 用于去调用认证管理器AuthenticationManager的认证方法authenticate()。最后验证登录成功。 完成后重启项目可以通过API文档的调试功能来测试登录如果使用无法登录的账号信息会在服务器端的控制台看到对应的异常 用户名不存在
org.springframework.security.authentication.InternalAuthenticationServiceException: UserDetailsService returned null, which is an interface contract violation 密码错误
org.springframework.security.authentication.BadCredentialsException: 用户名或密码错误 账号被禁用
org.springframework.security.authentication.DisabledException: 用户已失效
可以在全局异常处理器中添加处理以上异常的方法通常在处理时不会严格区分“用户名不存在”和“密码错误”这2种错误也就是说无论是这2种错误中的哪一种一般提示“用户名或密码错误”即可以进一步保障账号安全 关于以上用户名不存在、密码错误时对应的异常其继承结构是
AuthenticationException
-- BadCredentialsException // 密码错误
-- AuthenticationServiceException
-- -- InternalAuthenticationServiceException // 用户名不存在
则可以在处理异常的方法上在ExceptionHandler注解中指定需要处理的2种异常并且使用这2种异常公共的父类作为方法的参数如果光使用父类作为参数父类下的其他异常也会被处理所以要指定要处理的两种异常例如
// 如果ExceptionHandler没有配置参数则以方法参数的异常为准来处理异常
// 如果ExceptionHandler配置了参数则只处理此处配置的异常
ExceptionHandler({InternalAuthenticationServiceException.class,BadCredentialsException.class
})
public JsonResult handleAuthenticationException(AuthenticationException e) {// 暂不关心方法内部的代码
}
在实际处理时需要先在ServiceCode中添加新的枚举值以表示以上错误的状态码 然后在全局异常处理器中添加处理异常的方法
// 如果ExceptionHandler没有配置参数则以方法参数的异常为准来处理异常
// 如果ExceptionHandler配置了参数则只处理此处配置的异常
ExceptionHandler({InternalAuthenticationServiceException.class,BadCredentialsException.class
})
public JsonResult handleAuthenticationException(AuthenticationException e) {log.warn(程序运行过程中出现了AuthenticationException将统一处理);log.warn(异常, e);String message 登录失败用户名或密码错误;return JsonResult.fail(ServiceCode.ERR_UNAUTHORIZED, message);
}ExceptionHandler
public JsonResult handleDisabledException(DisabledException e) {log.warn(程序运行过程中出现了DisabledException将统一处理);log.warn(异常, e);String message 登录失败账号已经被禁用;return JsonResult.fail(ServiceCode.ERR_UNAUTHORIZED_DISABLE, message);
} 以上只能算验证已经完成了还不能算登录已经成功因为在判断用户名和密码对了以后还需要把相关的信息比如用户把它放进例如session里面去回头判断有没有登录的标准就是看session有没有这个信息有就是登录了没有就是没登录。所以登录不是判断用户名密码就结束还需要把信息留下来下次在来访问的时候才知道你是谁。不仅仅是验证的过程。 关于认证的标准
Spring Security为每个客户端分配了一个SecurityContext可称之为“Security上下文”并且会根据在SecurityContext中是否存在认证信息来判断当前请求是否已经通过认证即 如果在SecurityContext中存在有效的认证信息则视为“已通过认证” 如果在SecurityContext中没有有效的认证信息则视为“未通过认证”
所以在验证登录成功后需要将认证信息存入到SecurityContext中否则所开发的登录功能是没有意义的其实调用AuthenticationManager认证管理器对象的authenticate()方法时是可以接收到一个返回值的可以获取到认证结果。
使用SecurityContextHolder的getContext()静态方法可以获取当前客户端对应的SecurityContext对象 打印认证方法返回的结果 以上认证方法返回的结果例如
UsernamePasswordAuthenticationToken [Principalorg.springframework.security.core.userdetails.User [Usernameroot, Password[PROTECTED], Enabledtrue, AccountNonExpiredtrue, credentialsNonExpiredtrue, AccountNonLockedtrue, Granted Authorities[这是一个临时使用的山寨的权限]], Credentials[PROTECTED], Authenticatedtrue, Detailsnull, Granted Authorities[这是一个临时使用的山寨的权限]
]
其实以上数据是基于UserDetailsSerivce实现类中loadUserByUsername()返回的UserDetails对象来创建的
后续整个Spring Security在登录之后每次发请求的时候就可以重SecurityContext的到这个数据从而识别你的身份。 以上算是实现了一个登录的完整功能但是还有一个小的问题比如在登录了的时候服务端重启了此时登录的信息就没了此时在没有登录信息的时候去访问那些必须要登录的请求。会得到一个403错误。所以以下 未通过认证时拒绝访问
当未通过认证Spring Security从SecurityContext中未找到认证信息时尝试访问那些需要授权的资源不在白名单中的需要先登录才可以访问的资源在没有启用http.formLogin()时默认将响应403错误
需要在Spring Security的配置类中进行处理
首先用http去这个方法 这个方法需要传进去的参数的类型是AuthenticationEntryPoint点开后发现也是一个接口。 有两种方式可以自己写个类去实现但这个本身是一次性的使用因为这个类只用在配置里而配置本身是一次性的代码所以可以用匿名内部类来写。 这里我们需要向客户端去响应一个错误说你还没有登录那么可以直接用response去响应比如通过一个输出流-写出文本-关流响应一个简单内容 但是现在响应的内容太过简单可以响应一个message内容进去。 此时响应出现显示为一堆问号这是因为java原始的服务器端的问题默认使用的是ISO-8859-1这个编码格式这种格式是不支持中文的。 要在文档响应之前设置编码格式例如 此时显示就没有问题了 但此时任然不符合我们的设计需求。我们因该响应给客户端的是一个json结果而不是一个字符串而已需要更改文档类型前半截 在写入一个json格式的字符串格式可以复制手敲累容易出错 得到显示json的结果 现在代码恶心在需要自己去拼json这个结果最终我们要响应的还是和之前成功处理请求和处理异常时得到是一样的结果它依然是一个json格式的数据只不过我们之前处理请求处理异常返回JsonResult就可以为什么返回JsonResult的对象最终响应是一个json的数据是因为springMVC框架帮我们做了数据格式的转换转换成json格式的字符串。但现在不能转它不在springMVC的范围之内。 则需要人为创建JSON格式的结果可以借助fastjson工具进行处理这是一款可以实现对象与JSON格式字符串相互转换的工具需要添加依赖
fastjson.version1.2.75/fastjson.version
!-- fastjson实现对象与JSON的相互转换 --
dependencygroupIdcom.alibaba/groupIdartifactIdfastjson/artifactIdversion${fastjson.version}/version
/dependency 便可以用这个工具做以下调整 最终 目前登录算做好了对Security使用难得部分已经过去了下面是一些往后推进会设计的问题
识别当事人Principal
当事人当前提交请求的客户端的身份数据 。
当事人是一种身份数据作用是比如你登录一款软件这个软件得知道你是谁不然就无法做相关的操作例如登录之后你要修改自己的密码首先它得知道你是谁然后再去改你的密码。这份表示你到底是谁的这个数据其核心我们就把它叫做当事人。
当通过登录的验证后AuthenticationManager的authenticate()方法返回的Authentication对象中就包含了当事人信息例如
UsernamePasswordAuthenticationToken [Principalorg.springframework.security.core.userdetails.User [Usernameroot, Password[PROTECTED], Enabledtrue, AccountNonExpiredtrue, credentialsNonExpiredtrue, AccountNonLockedtrue, Granted Authorities[这是一个临时使用的山寨的权限]], Credentials[PROTECTED], Authenticatedtrue, Detailsnull, Granted Authorities[这是一个临时使用的山寨的权限]
] 数据里面的Principal这些数据就是当事人。
由于已经将以上认证结果存入到SecurityContext中则可以在后续任何需要识别当事人的场景中获取当事人信息
Spring Security提供了非常便利的获取当事人的做法在控制器类中的处理请求的方法的参数列表中可以声明当事人类型的参数这里的user就是当时返回的userDetails即可以说它是userDetails类型也可以说是User类型 并在参数上添加AuthenticationPrincipal注解即可例如找到管理员的controller 上面添加ApiIgnore是因为 写user有一个问题是API文档会以为你这个是请求参数会在API文档中看到很多参数调试里面也会有很多输入框需要加上这个注解来忽略。 此时这个user是有值的 它的值就是在登录成功后返回的当事人数据 也就是这一截
Principalorg.springframework.security.core.userdetails.User [Usernameroot, Password[PROTECTED], Enabledtrue, AccountNonExpiredtrue, credentialsNonExpiredtrue, AccountNonLockedtrue, Granted Authorities[这是一个临时使用的山寨的权限]]
就可以通过get拿到当时人的信息 完成以上代码后重启项目可以在API文档中使用各个账号尝试登录并访问以上“查询管理员列表”可以看到日志中输出了当次登录的账号的用户名例如 通过以上做法虽然可以获取当事人信息但是无论是UserDetails还是User类型可以获取的数据信息较少且不包含当前登录的用户的ID通常并不满足开发需求 需要记住当前在控制器类中处理请求的方法中注入的当事人数据就是UserDetailsService接口的实现类中返回的数据 而里面的数据来自于loginInfo loginInfo是从数据库查出来的 所以如果需要获取当事人的ID需要 在AdminLoginInfoVO中添加ID属性 修改Mapper层的getLoginInfoByUsername()需要查询管理员ID 现有的UserDetails的实现类User并不支持ID属性需要自定义类实现UserDetails接口或者自定义类继承自User类在自定义类中扩展出所需的各种属性例如ID
因为它本身给了我们user类 点开后发现user实现了UserDetails类 所以我们自定义继承user相对于也实现了UserDetails最终也可以作为这个方法的返回值。 在项目的根包下创建security.AdminDetails类继承自User类添加基于父类的构造方法并扩展出ID属性 然后只用第二个多的构造方法第一个可以去掉第二个包含了第一个所有的参数还有账户启动状态等必要的信息。 但同时也用不完第二个构造方法里面的所有参数我们需要把自己的构造方法中不用的参数去掉同时在调用父类的构造方法的时候需要这个参数我们在给个固定的值传过去就好了。 扩展出id属性并给构造参数加上id传进来给值。回头还需要被这个值取出来但是不能用Data因为Lombok需要在父类也就是user类有一个默认的无参构造方法但是user没有。所以添加Getter注解。 在UserDetailsService中返回数据时改为返回自定义类的对象其中将包含ID等属性值 里面的自定义的传参会略有不用之前判断账号是否禁用的0因为当时方法叫做disabled禁用而自己的属性的启用就用1判断。 添加权限用集合以下 最终代码如下 在控制器类中处理请求的方法中注入的当事人类型改为自定义类型 以上实现了可以登录登录后也知道你是谁的功能登录的效果就差不多了而Spring Security还有一个重要的功能就是权限我们可以区分不同的账户它有什么操作权限使得某些用户可以做特定的事情。如果要去判端当前这个人有没有权限去做这个事情第一件事是把现在给的山寨权限换成数据库里的真实权限。 实现根据权限限制访问 首先需要在管理员登录时明确此管理员的权限则需要在Mapper层实现“根据用户名查询管理员的登录信息且需要包含此管理员对应的各权限”需要执行的SQL语句大致是
selectams_admin.id,ams_admin.username,ams_admin.password,ams_admin.enable,ams_permission.value
from ams_admin
left join ams_admin_role on ams_admin.idams_admin_role.admin_id
left join ams_role_permission on ams_admin_role.role_idams_role_permission.role_id
left join ams_permission on ams_role_permission.permission_idams_permission.id
where usernameroot;
然后修改现有的查询功能需要先在AdminLoginInfoVO类中添加新的属性用于存放“权限列表” 然后调整AdminMapper.xml中的配置
!-- AdminLoginInfoVO getLoginInfoByUsername(String username); --
select idgetLoginInfoByUsername resultMapLoginInfoResultMapSELECTams_admin.id,ams_admin.username,ams_admin.password,ams_admin.enable,ams_permission.valueFROM ams_adminLEFT JOIN ams_admin_role ON ams_admin.idams_admin_role.admin_idLEFT JOIN ams_role_permission ON ams_admin_role.role_idams_role_permission.role_idLEFT JOIN ams_permission ON ams_role_permission.permission_idams_permission.idWHEREusername#{username}
/select!-- resultMap标签指导MyBatis封装查询结果 --
!-- resultMap标签的id属性自定义名称也是select标签上使用resultMap属性的值 --
!-- resultMap标签的type属性封装查询结果的类型的全限定名 --
resultMap idLoginInfoResultMaptypecn.tedu.csmall.passport.pojo.vo.AdminLoginInfoVO!-- id标签配置主键的列与属性的对应关系 --!-- result标签配置普通的列与属性的对应关系 --!-- collection标签配置List集合类型的属性与查询结果中的数据的对应关系 --!-- collection标签的ofType属性集合中的元素类型取值为类型的全限定名 --id columnid propertyid/result columnusername propertyusername/result columnpassword propertypassword/result columnenable propertyenable/collection propertypermissions ofTypeString!-- constructor标签通过构造方法来创建对象 --constructor!-- arg标签配置构造方法的参数如果构造方法有多个参数依次使用多个此标签 --arg columnvalue/arg/constructor/collection
/resultMap
补充解释关于使用resultMap标签
[SpringBoot]xml文件里写SQL用resultMap标签_万物更新_的博客-CSDN博客 配置完成后可以通过测试进行检验查询结果例如
根据【usernamesuper_admin】查询数据完成结果AdminLoginInfoVO(id2, usernamesuper_admin, password$2a$10$N.ZOn9G6/YLFixAOPMg/h.z7pCu6v2XyFDtC4q.jeeGm/TEZyj15C, enable1, permissions[/pms/product/read, /pms/product/add-new, /pms/product/delete, /pms/product/update, /pms/brand/read, /pms/brand/add-new, /pms/brand/delete, /pms/brand/update, /pms/category/read, /pms/category/add-new, /pms/category/delete, /pms/category/update, /pms/picture/read, /pms/picture/add-new, /pms/picture/delete, /pms/picture/update, /pms/album/read, /pms/album/add-new, /pms/album/delete, /pms/album/update]
) 基于方法的权限检查
以上loginInfo已经有真实的权限信息从中get出真实权限遍历加到权限集合里面去加进去后返回的userDetails就有真正的权限信息了。 当有了真实的权限以后接下来就可以对所有的访问加上权限的限制就是某些人可以干什么某些人不可以干什么。要实现这样的效果需要做两件事情。 第一件事情找到配置类 开启权限的检查机制 接下来就可以做访问什么需要什么权限例如必须具有管理员权限的值的人才可以查看权限列表。 加上下面这个注解后就表示你不光要登录你的认证信息的权限列表里面必须要包含hasAuthority里面的这个值才能够做这次的访问如果不包含这个权限就访问不了。 提示以上使用PreAuthorize注解检查权限时此注解可以添加在任何方法上例如Controller中的方法或Service中的方法等等由于当前项目中客户端的请求第一时间都是交给了Controller所以更适合在Controller方法上检查权限 当访问不包含所需的权限时 Spring Security给了我们以下这个异常 有异常在全局异常处理器里面处理异常 在ServiceCode中添加新的业务状态码表示“无此权限”
[异常]401和403的区分_万物更新_的博客-CSDN博客 然后在全局异常处理器中添加处理以上异常的方法 以上权限做好以后还需要给它添加Token功能这样每次客服端在访问过一次之后都不用在继续登陆。 [java]关于Session关于Token关于JWT_万物更新_的博客-CSDN博客
添加Token
首先添加Token-JWT的依赖项
父项目添加版本管理 父项目添加依赖 子项目添加依赖 添加好依赖以后做两个测试一个生成JWT的测试一个解析JWT 的测试
生成JWT
// 不太简单的、难以预测的字符串String secretKey jhdSfkkjKJ3831HdsDkdfSA9jklJD749Fhsa34fdsKf08dfjFhkdfs;Testvoid generate() {MapString, Object claims new HashMap();claims.put(id, 9527);claims.put(name, 张三);String jwt Jwts.builder()// Header.setHeaderParam(alg, HS256).setHeaderParam(typ, JWT)// Payload.setClaims(claims).setExpiration(new Date(System.currentTimeMillis() 3 * 60 * 1000))//设置有效期,防止一直用.// Verify Signature.signWith(SignatureAlgorithm.HS256, secretKey)// 生成.compact();System.out.println(jwt);}备注 基于它的做法我们可以自己传进去一个值 解析JWT // 不太简单的、难以预测的字符串String secretKey jhdSfkkjKJ3831HdsDkdfSA9jklJD749Fhsa34fdsKf08dfjFhkdfs; Testvoid parse() {String jwt eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoi5byg5LiJIiwiaWQiOjk1MjcsImV4cCI6MTY4NDkwODUwMn0.tBo7YKRqQv6TG2cf5jeu7nNjUim5X8H6pKLF1LrYuKI;Claims claims Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();Long id claims.get(id, Long.class);String name claims.get(name, String.class);System.out.println(id id);System.out.println(name name);}
备注
点进Claims可以看到本质是一个map 获取往里面放的值直接给的是object因为map的value被定义死了是object取出也是object 但在这里Claims在原有的map之上get方法是有扩展的传入的第二个参数就是你的目标类型是什么这样传进去是什么类型得到的就是什么类型。 以下是会这块会出现的异常列举出来回头需要全局处理。
如果尝试解析的JWT已经过期会出现异常
io.jsonwebtoken.ExpiredJwtException: JWT expired at 2023-05-24T12:02:38Z. Current time: 2023-05-24T14:04:35Z, a difference of 7317175 milliseconds. Allowed clock skew: 0 milliseconds. 如果解析JWT时使用的secretKey有误会出现异常
io.jsonwebtoken.SignatureException: JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.
如果解析JWT的数据格式错误会出现异常
io.jsonwebtoken.MalformedJwtException: JWT strings must contain exactly 2 period characters. Found: 1
补充 注意在不知晓secretKey的情况下也可以解析出JWT中的数据例如将JWT数据粘贴到官网只不过验证签名是失败的所以不要在JWT中存放敏感信息比如密码手机号码身份证号码等 验证签名是失败的就是说就算你知道里面的数据但是我会告诉你不可信比如id是9527但是也不要相信id就是9527因为它很有可能是一个伪造的JWT因为验证签名失败了。所以JWT的secretKey的价值是防止被伪造而不是防止被解析出来它不能做到这一点。 经过上面的测试接下来就要在项目中使用JWT识别用户的身份了 在项目中使用JWT识别用户的身份
核心流程 在项目中使用JWT识别用户的身份至少需要 当验证登录成功时生成JWT数据并响应到客户端去是“卖票”的过程 当验证登录成功后不再需要没有必要 当验证登录成功时生成JWT数据并响应到客户端去是“卖票”的过程 当验证登录成功后不再需要没有必要将认证结果存入到SecurityContext中 之前是这样的 当客户端提交请求时需要获取客户端携带的JWT数据并尝试解析解析成功后再将相关信息存入到SecurityContext中去因为之前我们说Security去检验这个账号或者说这次客户端的访问到底是不是一个已认证的状态就只是去看SecurityContext里面有没有东西所以一旦解析成功之后还是要把相关信息往SecurityContext里面放然后就没了后续说他有没有登录啊有没有权限啊不是这里管的事是Security去做后续的处理是“检票”的过程 可以调整Spring Security使用Session的策略改为不使用Session则不会将SecurityContext存入到Session中不存在Session里面的好处是它就只作用在这一次请求中这次请求结束了SecurityContext就没了当下次在过来的时候就又有了结束了又没了。。。所以SecurityContext里面的认证信息只作用于当次那一次而已在没有调整之前是基于session的意味着如果session的有效期是15分钟那你把认证信息存上下文里面那这个上下文的有效时间就是15分钟15分钟之内一直存在这个数据了如果有效期是30分钟那就会存在30分钟在这个30分钟里面肯定是会有浪费的时间的内存里面存这个信息就会浪费了并且在你重新来访之后时间又会重新调整为30分钟所以会有很长时间的浪费 验证登录成功时响应JWT
需要调整的代码大致包括 在IAdminService中将login()方法的返回值类型改为String类型重写的方法作同样的修改 在AdminServiceImpl中验证登录成功后生成此管理员的信息对应的JWT把上文测试里面生成JWT的代码拿过来做修改并返回 在AdminController中处理登录时调用Service方法时获取返回的JWT并响应到客户端去 解析客户端携带的JWT
客户端提交若干种不同的请求时可能都会携带JWT对应的在服务器处理若干种不同的请求时也都需要尝试接收并解析JWT则应该使用过滤器Filter组件进行处理
[web]关于过滤器Filter_万物更新_的博客-CSDN博客
其实Spring Security框架也使用了许多不同的过滤器来解决各种问题为了保证解析JWT是有效的解析JWT的代码必须运行在Spring Security的某些过滤器之前则接收、解析JWT的代码也必须定义在过滤器中 提示过滤器Filter是Java服务器端应用程序的核心组件之一它是最早接收到请求的组件过滤器可以对请求选择“阻止”或“放行”同一个项目中允许存在若干个过滤器形成“过滤器链Filter Chain”任何请求必须被所有过滤器都“放行”才会被控制器或其它组件所处理 按照之前的方法实现javax.servlet的过滤器接口让后重写doFilter方法。 但是重写方法需要对类型进行强转比较麻烦不太好用。 我们这次选择去继承Spring系列框架提供的OncePerRequestFilter这个类。 这个类是一个抽象类这个类继承自GenericFilterBean这个类。
而GenericFilterBean这个类实现了Filter这个接口 所以继承OncePerRequestFilter这个类也算是实现了过滤器接口的。 继承spring这个框架提供的OncePerRequestFilter这个类已经帮我们做了强转了就不用我们自己强转了。 所以在项目的根包下创建filter.JwtAuthorizationFilter类继承自OncePerRequestFilter类并添加Component注解
Slf4j
Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {log.debug(JwtAuthorizationFilter开始执行……);// 放行filterChain.doFilter(request, response);}}
添加Component注解把它标记成组件是因为通过注入把解析JWT的代码必须运行在Spring Security的某些过滤器之前。 到此可以测试通过API登录请求常看第一步过滤器有没有生效 关于携带JWT根据业内惯用的做法客户端会将JWT放在请求头Request Header中的Authorization属性中在Knife4j的API文档中可以 关于过滤器的初步实现
/*** JWT过滤器解决的问题接收JWT解析JWT将解析得到的数据创建为认证信息并存入到SecurityContext*/
Slf4j
Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {log.debug(JwtAuthorizationFilter开始执行……);// 根据业内惯用的做法客户端会将JWT放在请求头Request Header中的Authorization属性中String jwt request.getHeader(Authorization);log.debug(客户端携带的JWT{}, jwt);// 判断客户端是否携带了有效的JWTif (!StringUtils.hasText(jwt)) {// 如果JWT无效则放行并returefilterChain.doFilter(request, response);return;}// TODO 当前类和AdminServiceImpl中都声明了同样的secretKey变量是不合理的// TODO 解析JWT过程中可能出现异常需要处理// 尝试解析JWTString secretKey jhdSfkkjKJ3831HdsDkdfSA9jklJD749Fhsa34fdsKf08dfjFhkdfs;Claims claims Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();Long id claims.get(id, Long.class);String username claims.get(username, String.class);System.out.println(id id);System.out.println(username username);// TODO 需要考虑使用什么数据作为当事人// TODO 需要使用真实的权限// 创建认证信息Object principal username; //当事人 可以是任何类型暂时使用用户名Object credentials null; //凭证 本次不需要CollectionGrantedAuthority authorities new ArrayList();//权限authorities.add(new SimpleGrantedAuthority(山寨权限));Authentication authentication new UsernamePasswordAuthenticationToken(principal, credentials, authorities);// 将认证信息存入到SecurityContext中SecurityContext securityContext SecurityContextHolder.getContext();securityContext.setAuthentication(authentication);// 放行filterChain.doFilter(request, response);}}
因为上面代码中当事人是username此时参数再用AdminDetails 声明是不对的此处暂时去掉。 需要注意由于Spring Security的SecurityContext默认是基于Session的所以当携带JWT成功登录访问过后在SecurityContext中就已经有了认证信息并且在Session的有效期内即使后续不携带JWTSpring Security也能基于Session找到SecurityContext并读取到认证信息并不在需要登录就能访问的这可能与设计初衷并不相符 可以将Spring Security使用创建Session的策略改为“完全不使用Session”需要在Spring Security的配置类中添加配置 备注 1.用StringUtils.hasText的方法 用StringUtils.hasText的方法可以同时判断不能为空不能为null和包含文本。 包含文本即不是空白就是包含文本 2.关于 Object credentials null本此不需要凭证因为之前凭证的表现是密码而放在上下文里的认证信息作用是回头框架来识别出你是谁有什么权限这个过程是不需要使用密码的。 关于认证信息中的当事人
pring Security框架并不介意你使用什么类型作为认证信息Authentication中的当事人principal
在项目中到底使用什么类型作为当事人可以自行考虑主要考虑的因素就是当你需要注入当事人数据的时候你希望能够得到哪些数据
在项目的根包下创建security.LoginPrincipal作为自定义的当事人类型 并且在解析JWT成功后在过滤去使用此类型作为当事人来创建认证信息 后续在Controller中就可以通过AuthenticationPrincipal来注入自定义的当事人数据例如 接着处理一个小问题因为在生成和解析JWT的时候对需要用到secretKey这个值并且这个值相同如果不相同就会签名失败所以一个完全相同的代码写两遍是不合理的有两种解决方案第一种是专门写一个类去调取第二个是写在application.yml文件里面它们的区别是在application.yml里面需要读取在应用有一个读取的过程在类里面是直接应用的从执行效率来说肯定是在类里面更快一些但由于这个值需要甲方来定为了防止伪造相关问题所以必须写在application.yml里面。 关于secretKey必须有4位以上否则都会被视为空值报错 以上权限还是一个假的权限需要换成真的权限目前我们就用把权限放在JWT中然后再从JWT中取出权限的方式。以替换在数据库里查的方式因为从数据库里查数据是一个效率低下的方式其一需要连接传递SQL然后准备准备好了编译执行执行好了在给个结果一个过程。其二数据库里面的数据存在硬盘里面硬盘是一个存储效率非常低效的硬件。同时这段代码只要有客户端来访就会执行这段代码发生的非常高频率所以不能选择连接数据库这么低效的做法 把集合放进JWT里面。 从JWT取出权限列表 这样的取出方式看似语法没有问题但会出现类型转换错误。 因为在这一步它获取出来的是LinkedHashMap但是LinkedHashMap不能强制转其他类型为什么获取的是LinkedHashMap类型呢因为API不知道你要获取什么类型给你处理为了LinkedHashMap。因为是集合加泛型也没有办法向获取id一样在后面第二个参数加上Long.class来指定返回的类型。 所以这里需要换一个做法在生成JWT的时候不往里面放集合里改为放Json 可以放Json是因为我们有添加fastjson的依赖实现对象和Json相互转换的依赖。 在从JWT 取出权限的时候也取出Json字符串然后用fastjson转成集合 以上就实现真实权限的功能了。 注意此方式也不是最优解决方案。 接下来处理解析JWT时可能出现的异常往常我们是在全局异常处理器处理的但是在这里不行因为解析JWT是在过滤器里面做的全局异常处理器只能处理controller抛出的异常。 处理解析JWT时的异常
由于解析JWT是在过滤器组件中执行的而过滤器是最早处理请求的组件此时控制器Controller还没有开始处理这次的请求则全局异常处理器也无法处理解析JWT时出现的异常全局异常处理器只能处理控制器抛出的异常这里使用最原始的try...catch处理
首先在ServiceCode中补充新的状态码
ERR_JWT_EXPIRED(60000),
ERR_JWT_MALFORMED(60100),
ERR_JWT_SIGNATURE(60200),
然后在JwtAuthorizationFilter中使用try...catch包裹尝试解析JWT的代码
// 尝试解析JWT
response.setContentType(application/json; charsetutf-8);
Claims claims null;
try {claims Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
} catch (MalformedJwtException e) {String message 非法访问;log.warn(程序运行过程中出现了MalformedJwtException将向客户端响应错误信息);log.warn(错误信息{}, message);JsonResult jsonResult JsonResult.fail(ServiceCode.ERR_JWT_MALFORMED, message);String jsonString JSON.toJSONString(jsonResult);PrintWriter printWriter response.getWriter();printWriter.println(jsonString);printWriter.close();return;
} catch (SignatureException e) {String message 非法访问;log.warn(程序运行过程中出现了SignatureException将向客户端响应错误信息);log.warn(错误信息{}, message);JsonResult jsonResult JsonResult.fail(ServiceCode.ERR_JWT_SIGNATURE, message);String jsonString JSON.toJSONString(jsonResult);PrintWriter printWriter response.getWriter();printWriter.println(jsonString);printWriter.close();return;
} catch (ExpiredJwtException e) {String message 您的登录信息已经过期请重新登录;log.warn(程序运行过程中出现了ExpiredJwtException将向客户端响应错误信息);log.warn(错误信息{}, message);JsonResult jsonResult JsonResult.fail(ServiceCode.ERR_JWT_EXPIRED, message);String jsonString JSON.toJSONString(jsonResult);PrintWriter printWriter response.getWriter();printWriter.println(jsonString);printWriter.close();return;
} catch (Throwable e) {String message 服务器忙请稍后再试【在开发过程中如果看到此提示应该检查服务器端的控制台分析异常并在解析JWT的过滤器中补充处理对应异常的代码块】;log.warn(程序运行过程中出现了Throwable将向客户端响应错误信息);log.warn(异常, e);JsonResult jsonResult JsonResult.fail(ServiceCode.ERR_UNKNOWN, message);String jsonString JSON.toJSONString(jsonResult);PrintWriter printWriter response.getWriter();printWriter.println(jsonString);printWriter.close();return;
}
注意
以上代码中只有response.setContentType(application/json; charsetutf-8);这串代码可以提到最上面给每一个catch复用。 PrintWriter printWriter response.getWriter();是不可以的 把printWriter 放在上面会导致本该在正常成功访问的时候会报状态异常错误说getWriter在本次调用中已经被占用了。原因是我们服务端向客户端响应就是用printWriter 来响应的然后你在上图中拿到了getWriter输出流控制器那边就拿不到响应成功的输出流了以至于控制器没有办法去响应。 以上JWT就差不多了以下在和前端结合的时候还需要实现的一些功能。 处理复杂请求的跨域问题
当客户端提交请求时在请求头中配置了特定的属性例如Authorization带了JWT的时候则这个请求会被视为“复杂请求” 对于复杂请求浏览器会先对服务器端发送OPTIONS类型的请求也是和getpost一样的请求方式OPTIONS请求的目的是试一下服务器是不是好的是不是可以接受以执行预检PreFlight如果预检通过才会执行本应该发送的请求。
然后会看到它的请求就需要给它配置白名单已通过。 在Spring Security的配置类中可以在配置对请求授权时将所有OPTIONS类型的请求全部直接许可例如 或者调用参数对象的cors()方法也可以例如 提示对于复杂请求的预检是浏览器的行为并且当某个请求通过预检后浏览器会缓存此结果后续再次发出此请求时不会再次执行预检。 实现单点登录以下 单点登录
SSOSingle Sign On单点登录表示在集群或分布式系统中客户端只需要在某1个服务器上完成登录的验证后续无论访问哪个服务器都不需要再次重新登录常见的实现手段主要有共享Session使用Token。 目前如果希望客户端在csmall-passport中登录后在csmall-product中也能够被识别身份、权限需要 复制依赖项spring-boot-starter-security、jjwt、fastjson 复制LoginPrincipal 复制ServiceCode覆盖此前的文件 复制application-dev.yml中的自定义的配置 复制JwtAuthorizationFilter 复制SecurityConfiguration并更改导包 删除PasswordEncoder的Bean方法 删除AuthenticationManager的Bean方法 删除“白名单”中管理员登录的URL地址
完成后在csmall-product项目中也可以通过AuthenticationPrincipal来注入当事人数据也可以使用PreAuthorize来配置访问权限这些都是通的。