食品网站建设策划方案,wordpress电商网站,微网站开发报价单,没有网站怎么做链接视频播放器文/朱季谦 这是一个古老的传说。
在神秘的Web系统世界里#xff0c;有一座名为Spring Security的山谷#xff0c;它高耸入云#xff0c;蔓延千里#xff0c;鸟飞不过#xff0c;兽攀不了。这座山谷只有一条逼仄的道路可通。然而#xff0c;若要通过这条道路前往另一头的…文/朱季谦 这是一个古老的传说。
在神秘的Web系统世界里有一座名为Spring Security的山谷它高耸入云蔓延千里鸟飞不过兽攀不了。这座山谷只有一条逼仄的道路可通。然而若要通过这条道路前往另一头的世界就必须先拿到一块名为token的令牌只有这样道路上戍守关口的士兵才会放行。 想要获得这块token令牌必须带着一把有用的userName钥匙和password密码进入到山谷深处找到藏匿宝箱的山洞数据库若能用钥匙打开其中一个宝箱就证明这把userName钥匙是有用的。正常情况下宝箱里会有一块记录各种信息的木牌包含着钥匙名和密码其密码只有与你所携带的密码检验一致时才能继续往前走得到的通行信息将会在下一个关口处做认证进而在道路尽头处的JWT魔法屋里获得加密的token令牌。
慢着既然山谷关口处有士兵戍守令牌又在山谷当中在还没有获得令牌的情况下又怎么能进入呢
设置关口的军官早已想到这种情况因此他特意设置了一条自行命名为“login”的道路没有令牌的外来人员可从这条道路进入山谷去寻找传说中的token令牌。这条道路仅仅只能进入到山谷却无法通过山谷到达另一头的世界因此它更像是一条专门为了给外来人员获取token令牌而开辟出来的道路。 这一路上会有各种关口被士兵把守检查只有都一一通过了才能继续往前走路上会遇到一位名为ProviderManager的管理员他管理着所有信息提供者Provider......需找到一位可正确带路的信息提供者Provider在他的引导下前往山洞数据库成功获取到宝箱拿到里面记录信息的木牌这样方能验证所携带的username和password是否正确。若都正确那么接下来就可将信息进行认证并前往JWT魔法屋获取token令牌。最后携带着token返回到家乡让族人都可穿过山谷而进入到web系统去获取更多珍贵的资源。
这就是整个security的游戏规则原理。
那么在游戏开始之前我们先了解下当年戍守山谷的军官是如何设置这道权限关口的......
关口的自定义设置主要有三部分通过钥匙username获取到宝箱宝箱里的UserDetails通行信息设置关口通行过往检查SecurityConfig设置。
1.宝箱里的通行信息: 1 /**2 * 安全用户模型3 *4 * author zhujiqian5 * date 2020/7/30 15:276 */7 public class JwtUserDetails implements UserDetails {8 private static final long serialVersionUID 1L;9 10 private String username;11 private String password;12 private String salt;13 private Collection? extends GrantedAuthority authorities;14 15 JwtUserDetails(String username, String password, String salt, Collection? extends GrantedAuthority authorities) {16 this.username username;17 this.password password;18 this.salt salt;19 this.authorities authorities;20 }21 22 Override23 public String getUsername() {24 return username;25 }26 27 JsonIgnore28 Override29 public String getPassword() {30 return password;31 }32 33 public String getSalt() {34 return salt;35 }36 37 Override38 public Collection? extends GrantedAuthority getAuthorities() {39 return authorities;40 }41 42 JsonIgnore43 Override44 public boolean isAccountNonExpired() {45 return true;46 }47 48 JsonIgnore49 Override50 public boolean isAccountNonLocked() {51 return true;52 }53 54 JsonIgnore55 Override56 public boolean isCredentialsNonExpired() {57 return true;58 }59 60 JsonIgnore61 Override62 public boolean isEnabled() {63 return true;64 }65 66 }这里JwtUserDetails实现Spring Security 里的UserDetails类这个类是长这样的下面对各个字段做了注释 1 public interface UserDetails extends Serializable {2 /**3 *用户权限集默认需要添加ROLE_前缀4 */5 Collection? extends GrantedAuthority getAuthorities();6 7 /**8 *用户的加密密码不加密会使用{noop}前缀9 */10 String getPassword();11 12 /**13 *获取应用里唯一用户名14 */15 String getUsername();16 17 /**18 *检查账户是否过期19 */20 boolean isAccountNonExpired();21 22 /**23 *检查账户是否锁定24 */25 boolean isAccountNonLocked();26 27 /**28 *检查凭证是否过期29 */30 boolean isCredentialsNonExpired();31 32 /**33 *检查账户是否可用34 */35 boolean isEnabled();36 }说明JwtUserDetails自定义实现了UserDetails类增加username和password字段除此之外还可以扩展存储更多用户信息例如身份证手机号邮箱等等。其作用在于可构建成一个用户安全模型用于装载从数据库查询出来的用户及权限信息。
2.通过钥匙username获取到宝箱方法: 1 /**2 * 用户登录认证信息查询3 *4 * author zhujiqian5 * date 2020/7/30 15:306 */7 Service8 public class UserDetailsServiceImpl implements UserDetailsService {9 10 Resource11 private SysUserService sysUserService;12 13 Override14 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {15 SysUser user sysUserService.findByName(username);16 if (user null) {17 throw new UsernameNotFoundException(该用户不存在);18 }19 20 SetString permissions sysUserService.findPermissions(user.getName());21 ListGrantedAuthority grantedAuthorities permissions.stream().map(AuthorityImpl::new).collect(Collectors.toList());22 return new JwtUserDetails(user.getName(), user.getPassword(), user.getSalt(), grantedAuthorities);23 }24 }这个自定义的UserDetailsServiceImpl类实现了Spring Security框架自带的UserDetailsService接口这个接口只定义一个简单的loadUserByUsername方法 1 public interface UserDetailsService {2 UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;3 }根据loadUserByUsername方法名便能看出这是一个可根据username用户名获取到User对象信息的方法并返回一个UserDetails对象即前头的“宝箱里的通行信息”换言之通过重写这个方法我们能在该方法里实现用户登录认证信息的查询并返回对应查询信息。
综合以上代码先用开头提到的宝箱意象做一个总结即拿着userName这把钥匙通过loadUserByUsername这个方法指引可进入到山洞数据库去寻找能打开的宝箱在数据库里select查询userName对应数据若能打开其中一个宝箱即数据库里存在userName对应的数据则获取宝箱里的通行信息实现UserDetails的JwtUserDetails对象信息。
3.关口通行过往检查设置
自定义的SecurityConfig配置类是SpringBoot整合Spring Security的关键灵魂所在。该配置信息会在springboot启动时进行加载。其中authenticationManager() 会创建一个可用于传token做认证的AuthenticationManager对象而AuthenticationManagerBuilder中的auth.authenticationProvider()则会创建一个provider提供者并将userDetailsService注入进去该userDetailsService的子类被自定义的UserDetailsServiceImpl类继承并重写loadUserByUsername()方法因此当源码里执行userDetailsService的loadUserByUsername()方法时即会执行被重写的子类loadUserByUsername()方法。
由此可见在做认证的过程中只需找到注入userDetailsService的provider对象即可执行loadUserByUsername去根据username获取数据库里信息。
那具体是在哪个provider对象请看下面详细解析。 1 Configuration2 EnableWebSecurity3 EnableGlobalMethodSecurity(prePostEnabled true)4 public class SecurityConfig extends WebSecurityConfigurerAdapter {5 6 Resource7 private UserDetailsService userDetailsService;8 9 Override10 public void configure(AuthenticationManagerBuilder auth) {11 auth.authenticationProvider(new JwtAuthenticationProvider(userDetailsService));12 }13 14 Bean15 Override16 public AuthenticationManager authenticationManager() throws Exception {17 return super.authenticationManager();18 }19 20 Override21 protected void configure(HttpSecurity httpSecurity) throws Exception {22 //使用的是JWT禁用csrf23 httpSecurity.cors().and().csrf().disable()24 //设置请求必须进行权限认证25 .authorizeRequests()26 //跨域预检请求27 .antMatchers(HttpMethod.OPTIONS, /**).permitAll()28 //permitAll()表示所有用户可认证29 .antMatchers( /webjars/**).permitAll()30 //首页和登录页面31 .antMatchers(/).permitAll()32 .antMatchers(/login).permitAll()33 // 验证码34 .antMatchers(/captcha.jpg**).permitAll()35 // 其他所有请求需要身份认证36 .anyRequest().authenticated();37 //退出登录处理38 httpSecurity.logout().logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler());39 //token验证过滤器40 httpSecurity.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);41 }42 }首先双击SecurityConfig 类里的JwtAuthenticationProvider—— 进入到JWTAuthenticationProvider类内部发现原来该类是继承了DaoAuthenticationProvider。 请注意这段话很关键
点击setUserDetailsService(userDetailsService)。进入到方法里面后发现这里其实是把UserDetailsService通过set方式依赖注入到DaoAuthenticationProvider类中换言之我们接下来在加载完成的框架里只需通过DaoAuthenticationProvider的getUserDetailsService()方法便可获取前面注入的userDetailsService进而调用其子类实现的loadUserByUsername()方法。
看到这里您须重点关注一下DaoAuthenticationProvider这个类它将会在后面再次与我们碰面而它是一个AuthenticationProvider。
若您还不是很明白AuthenticationProvider究竟是什么那就暂且统一把它当做信息提供者吧而它是ProviderManager管理员底下其中一个信息提供者Provider。 写到这里还有一个疑问即security框架是如何将信息提供者Provider归纳到ProviderManager管理员手下的呢
解答这个问题需回到SecurityConfig配置文件里点击authenticationProvider进入到底层方法当中。 进入后里面是具体的方法实现大概功能就是把注入了userDetailsService的信息提供者DaoAuthenticationProvider添加到一个List集合里然后再将集合里的所有提供者通过构造器传入ProviderManager命名生成一个新的提供者管理员providerManager。这里面还涵盖不少细节感兴趣的读者可自行再扩展深入研究。 以上就初步设置好了游戏规则。
接下来就是主角上场了。
在所有的游戏里都会有一个主角而我们这个故事自然也不例外。 此时在一扇刻着“登录”二字的大门前有一个小兵正在收拾他的包袱准备跨过大门踏上通往Spring Security山谷的道路。他背负着整个家族赋予的任务需前往Security山谷拿到token令牌只有把它成功带回来家族里的其他成员才能有机会穿过这座山谷前往另一头的神秘世界获取到珍贵的资源。
这个小兵便是我们这故事里的主角我把他叫做线程他将带着整个线程家族的希望寻找可通往神秘系统世界的令牌。
线程把族长给予的钥匙和密码放进包袱他回头看了一眼自己的家乡然后挥了挥手跨过“登录”这扇大门勇敢地上路了。
线程来到戒备森严的security关口前四周望了一眼忽然发现关口旁立着一块显眼的石碑上面刻着一些符号。他走上前一看发现原来是当年军官设置的指令与对应的说明 1 Override2 protected void configure(HttpSecurity httpSecurity) throws Exception {3 //使用的是JWT禁用csrf4 httpSecurity.cors().and().csrf().disable()5 //设置请求必须进行权限认证6 .authorizeRequests()7 //跨域预检请求8 .antMatchers(HttpMethod.OPTIONS, /**).permitAll()9 //首页和登录页面10 .antMatchers(/login).permitAll()11 // 其他所有请求需要身份认证12 .anyRequest().authenticated();13 //退出登录处理14 httpSecurity.logout().logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler());15 //token验证过滤器16 httpSecurity.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);17 }其中permitAll()代表所有请求都可访问当它设置成类似“.antMatchers(/login).permitAll()”的形式时则代表该/login路径请求无需认证便可通过相反代码anyRequest().authenticated()则意味着其他的所有请求都必须进行身份验证方能通过否则会被拒绝访问。
下面将通过debug一步一步揭示线程是如何闯关升级的最后成功获取到传说中的token令牌。
线程来到关口处不久在戍守士兵的指引下开始往login道路走去前面迎接他将是一系列的关口检查。
1.传入userNamepassword属性封装成一个token对象。 进入到该对象里可看到用户名赋值给this.principal密码赋值给this.credentials其中setAuthenticated(false)意味着尚未进行认证。 注意一点是UsernamePasswordAuthenticationToken继承了AbstractAuthenticationToken而AbstractAuthenticationToken实现Authentication由传递关系可知Authentication是UsernamePasswordAuthenticationToken的基类故而UsernamePasswordAuthenticationToken是可以向上转换为Authentication理解这一点就能明白为何接下来authenticationManager.authenticate(token)方法传进去的是UsernamePasswordAuthenticationToken但在源码里方法参数则为Authentication。
2.将usernamepassword封装成token对象后通过Authentication authenticationauthenticationManager.authenticate(token)方法进行认证里面会执行一系列认证操作需要看懂源码才能知道这行代码背后藏着的水月洞天然有一点是可以从表面上看懂的即若成功认证通过将会返回一个认证成功的Authentication对象至于对象里是什么信息请继续 往下看。 3.点击进入到AuthenticationManager里发现该接口里只有一个方法 1 Authentication authenticate(Authentication authentication)2 throws AuthenticationException;由此可知它的具体实现是通过实现类来操作的它的主要实现类有N多个其中在认证过程中我们需关注的是ProviderManager这个类。 这个ProviderManager即前面提到的Provider管理员他管理着一堆信息提供者provider。线程此行的目的就是先找到这个Provider管理员再去管理员手中寻找能够匹配到的提供者provider只有通过匹配到的提供者才能找到获取数据库的方法loadUserByUsername。
4.ProviderManager类实际上是实现AuthenticationManager接口重写了authenticate方法。因此当前面代码执行authenticationManager.authenticate(token)方法时具体实现将由其子类重写的方法操作子类即ProviderManager。 debug进去后—— 继续往下执行通过getProviders() 可获取到内部维护在List中的AuthenticationProvider遍历进行验证若该提供者能支持传入的token进行验证则继续往下执行。 其中JwtAuthAuthenticationProvider可执行本次验证而JwtAuthAuthenticationProvider是继承DaoAuthenticationProvider后自定义的类可以理解成进行认证验证的Provider是前面重点提到的DaoAuthenticationProvider。 DaoAuthenticationProvider是一个具体实现类它继承AbstractUserDetailsAuthenticationProvider抽象类。 而AbstractUserDetailsAuthenticationProvider实现了AuthenticationProvider接口。 5.在ProviderManager中执行到result provider.authenticate(authentication)时其中provider是由AuthenticationProvider定义的但AuthenticationProvider是一个接口需由其子类具体实现。根据上面分析可知AbstractUserDetailsAuthenticationProvider会具体实现provider.authenticate(authentication)方法。debug进入到其authenticate方法当中会跳转到AbstractUserDetailsAuthenticationProvider重写的authenticate()方法当中接下来会详细介绍该authenticate()执行的代码模块 5.1.首先第一步会执行this.userCache.getUserFromCache(username)获取缓存里的信息。 5.2 若缓存里没有UserDetails信息将会继续往下执行执行到retrieveUser方法该方法的总体作用是通过登录时传入的userName去数据库里做查询若查询成功便将数据库的User信息包装成UserDetails对象返回当然具体如何从数据库里获取到信息则需要重写一个方法即前面提到的loadUserByUsername()方法。
值得注意一点是一般新手接触到security框架都会有一个疑问即我登录时传入了username是如何获取到数据库里的用户信息
其实这个疑问的关键答案就藏在这个retrieveUser()方法里。该方法名的英文解析是“训练成能寻回猎物的猎犬”。我觉得这个翻译在这里很有意思暂且可以把它当成信息提供者Provider驯养的一头猎犬它可以帮我们的游戏主角线程在茫茫的森林里寻找到藏匿宝箱的山洞数据库。 5.3 接下来就让这头猎犬给我们带路吧——点击retrieveUser()进入到方法当中发现这其实是一个抽象方法故而其具体实现将在子类中进行。 5.4 进入到其子类实现的方法当中发现会进入前面提到AbstractUserDetailsAuthenticationProvider的子类DaoAuthenticationProvider它也是一个AuthenticationProvider即所谓的信息提供者之一。在DaoAuthenticationProvider类里实现了父类的retrieveUser方法。
在猎犬的retrieveUser的带路下我们最后看到 了熟悉的老朋友关键方法loadUserByUserName()。 点进loadUserByUsername()方法里会进入到UserDetailsService接口里该接口只有loadUserByUsername一个方法该方法具体在子类里实现。 这个接口被我们自定义重写了即前面露过面的 在DaoAuthenticationProvider类中调用loadUserByUserName()方法时最终会执行我们重写的loadUserByUsername()方法该方法将会去数据库里查询username的信息并返回一个User对象最后SysUser对象转换成UserDetails返回给DaoAuthenticationProvider对象里的UserDetails跳转如下图 5.5 DaoAuthenticationProvider的retirieveUser执行完后会将数据库查询到的UserDetails返回给上一层即AbstractUserDetailsAuthenticationProvider执行的retrieveUser()方法得到的UserDetails赋值给user。 6.接下来就是各种检查其中有一个检查方法需要特别关注即 注additionalAuthenticationChecks()方法的作用是检查密码是否一致的前面已根据username去数据库里查询出user数据接下来就需要在该方法里检查数据库里user的密码与登录时传入的密码是否一致了。
6.1 点击additionalAuthenticationChecks()进入到方法里发现AbstractUserDetailsAuthenticationProvider当中的additionalAuthenticationChecks同样是一个抽象方法没有具体实现它与前面的retrieveUser()方法一样具体实现都在AbstractUserDetailsAuthenticationProvider的子类DaoAuthenticationProvider中重写了。 6.2.跳转进入子类重写的additionalAuthenticationChecks()当中先通过authentication.getCredentials().toString()从token对象中获取登录时输入的密码再通过passwordEncoder.matches(presentedPassword, userDetails.getPassword())进行比较即拿登录的密码与数据库里取出的密码做对比执行到这一步若两个密码一致时即登录的username和password能与数据库里某个username和密码匹配则可登录成功。 7.用户名与密码都验证通过后可继续执行下一步操作中间还有几个检查方法读者若感兴趣可自行研究。最后会把user赋值给一个principalToReturn对象然后连同authentication还有user一块传入到createSuccessAuthentication方法当中。 8.在createSuccessAuthentication方法里会创建一个已经认证通过的token。 点进该token对象当中可以看到这次的setAuthenticated设置成了true即意味着已经认证通过。 最后将生成一个新的token并以Authentication对象形式返回到最开始的地方。 执行到这一步就可以把认证通过的信息进行存储到这里就完成了核心的认证部分。
接下来我们的主角线程就可以前往JWT魔法屋获取加密的token令牌然后携带令牌返回故土届时其线程家族里的其他成员都可穿过这座Spring Security山谷前往山谷另一边的web系统世界了。 那是另外一个世界的故事我们将在以后漫长的岁月当中缓缓道来.....
而这个关于Spring Security山谷的故事就暂且记到这里若当中有不当之处还需各位大佬指出而加以改进。 本文完插图皆来自网络。