首页 文章 精选 留言 我的

精选列表

搜索[分代式],共10000篇文章
优秀的个人博客,低调大师

十分钟带你轻松入门Shiro

Shiro集成Spring 首先集成Spring、SpringMVC和Shiro <dependencies><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>4.3.18.RELEASE</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId><version>4.3.18.RELEASE</version></dependency><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-all</artifactId><version>1.3.2</version></dependency><dependency><groupId>net.sf.ehcache</groupId><artifactId>ehcache-core</artifactId><version>2.6.2</version></dependency></dependencies> 在web.xml文件中配置Shiro的过滤器 <!--1.配置Shiro的shiroFilter.2.DelegatingFilterProxy实际上是Filter的一个代理对象.默认情况下,Spring会到IOC容器中查找和<filter-name>对应的filterbean.也可以通过targetBeanName的初始化参数来配置filterbean的id.--><filter><filter-name>shiroFilter</filter-name><filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class><init-param><param-name>targetFilterLifecycle</param-name><param-value>true</param-value></init-param></filter><filter-mapping><filter-name>shiroFilter</filter-name><url-pattern>/*</url-pattern></filter-mapping> 创建Shiro的配置文件(ehcache-shiro.xml) <ehcacheupdateCheck="false"name="shiroCache"><defaultCachemaxElementsInMemory="10000"eternal="false"timeToIdleSeconds="120"timeToLiveSeconds="120"overflowToDisk="false"diskPersistent="false"diskExpiryThreadIntervalSeconds="120"/></ehcache> 在Spring的配置文件中对Shiro进行配置 <?xmlversion="1.0"encoding="UTF-8"?><beansxmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsd"><!--1.配置SecurityManager!--><beanid="securityManager"class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"><propertyname="cacheManager"ref="cacheManager"/><propertyname="authenticator"ref="authenticator"/></bean><!--2.配置CacheManager.2.1需要加入ehcache的jar包及配置文件.--><beanid="cacheManager"class="org.apache.shiro.cache.ehcache.EhCacheManager"><propertyname="cacheManagerConfigFile"value="classpath:ehcache-shiro.xml"/></bean></bean><!--=========================================================ShiroSpring-specificintegration=========================================================--><!--Postprocessorthatautomaticallyinvokesinit()anddestroy()methodsforSpring-configuredShiroobjectssoyoudon'thaveto1)specifyaninit-methodanddestroy-methodattributesforeverybeandefinitionand2)evenknowwhichShiroobjectsrequirethesemethodstobecalled.--><!--4.配置LifecycleBeanPostProcessor.可以自动调用配置在SpringIOC容器中shirobean的生命周期方法.--><beanid="lifecycleBeanPostProcessor"class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/><!--5.启用IOC容器中使用shiro的注解.但必须在配置了LifecycleBeanPostProcessor之后才可以使用.--><beanclass="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"depends-on="lifecycleBeanPostProcessor"/><beanclass="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"><propertyname="securityManager"ref="securityManager"/></bean><!--6.配置ShiroFilter.6.1id必须和web.xml文件中配置的DelegatingFilterProxy的<filter-name>一致.若不一致,则会抛出:NoSuchBeanDefinitionException.因为Shiro会来IOC容器中查找和<filter-name>名字对应的filterbean.--><beanid="shiroFilter"class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"><propertyname="securityManager"ref="securityManager"/><propertyname="loginUrl"value="/login.jsp"/><propertyname="successUrl"value="/list.jsp"/><propertyname="unauthorizedUrl"value="/unauthorized.jsp"/><!--配置哪些页面需要受保护.以及访问这些页面需要的权限.1).anon可以被匿名访问2).authc必须认证(即登录)后才可能访问的页面. 3). logout 登出.4).roles角色过滤器--><propertyname="filterChainDefinitions"><value>/login.jsp=anon#everythingelserequiresauthentication:/**=authc</value></property></bean></beans> 配置完成,启动项目即可 工作流程 在这里插入图片描述 Shiro通过在web.xml配置文件中配置的ShiroFilter来拦截所有请求,并通过配置filterChainDefinitions来指定哪些页面受保护以及它们的权限。 URL权限配置 [urls]部分的配置,其格式为:url=拦截器[参数];如果当前请求的url匹配[urls]部分的某个url模式(url模式使用Ant风格匹配),将会执行其配置的拦截器,其中: anon:该拦截器表示匿名访问,即不需要登录便可访问 authc:该拦截器表示需要身份认证通过后才可以访问 logout:登出 roles:角色过滤器 例: <propertyname="filterChainDefinitions"><value>/login.jsp=anon#everythingelserequiresauthentication:/**=authc</value></property> 需要注意的是,url权限采取第一次匹配优先的方式,即从头开始使用第一个匹配的url模式对应的拦截器链,如: /bb/**=filter1 /bb/aa=filter2 /**=filter3 如果请求的url是/bb/aa,因为按照声明顺序进行匹配,那么将使用filter1进行拦截。 Shiro认证流程 获取当前的Subject —— SecurityUtils.getSubject() 校验当前用户是否已经被认证 —— 调用Subject的isAuthenticated()方法 若没有被认证,则把用户名和密码封装为UsernamePasswordToken对象 执行登录 —— 调动Subject的login(UsernamePasswordToken)方法 自定义Realm的方法,从数据库中获取对应的记录,返回给Shiro 自定义类继承org.apache.shiro.realm.AuthenticatingRealm 实现doGetAuthenticationInfo(AuthenticationToken)方法 由Shiro完成对用户名密码的比对 下面具体实现一下,首先创建login.jsp: <%@pagecontentType="text/html;charset=UTF-8"language="java"%><html><head><title>Title</title></head><body><h4>LoginPage</h4><formaction="shiroLogin"method="post">username:<inputtype="text"name="username"/><br/><br/>password:<inputtype="password"name="password"/><br/><br/><inputtype="submit"value="Submit"/></form></body></html> 然后编写控制器: packagecom.wwj.shiro.handlers;importorg.apache.shiro.SecurityUtils;importorg.apache.shiro.authc.AuthenticationException;importorg.apache.shiro.authc.UsernamePasswordToken;importorg.apache.shiro.subject.Subject;importorg.springframework.stereotype.Controller;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RequestParam;@ControllerpublicclassShiroHandler{@RequestMapping("/shiroLogin")publicStringlogin(@RequestParam("username")Stringusername,@RequestParam("password")Stringpassword){//获取当前的SubjectSubjectcurrentUser=SecurityUtils.getSubject();//校验当前用户是否已经被认证if(!currentUser.isAuthenticated()){//把用户名和密码封装为UsernamePasswordToken对象UsernamePasswordTokentoken=newUsernamePasswordToken(username,password);token.setRememberMe(true);try{//执行登录currentUser.login(token);}catch(AuthenticationExceptionae){System.out.println("登录失败"+ae.getMessage());}}return"redirect:/list.jsp";}} 编写自定义的Realm: packagecom.wwj.shiro.realms;importorg.apache.shiro.authc.*;importorg.apache.shiro.realm.AuthenticatingRealm;publicclassShiroRealmextendsAuthenticatingRealm{/***@paramauthenticationToken该参数实际上是控制器方法中封装用户名和密码后执行login()方法传递进去的参数token*@return*@throwsAuthenticationException*/@OverrideprotectedAuthenticationInfodoGetAuthenticationInfo(AuthenticationTokenauthenticationToken)throwsAuthenticationException{//将参数转回UsernamePasswordTokenUsernamePasswordTokentoken=(UsernamePasswordToken)authenticationToken;//从UsernamePasswordToken中取出用户名Stringusername=token.getUsername();//调用数据库方法,从数据表中查询username对应的记录System.out.println("从数据库中获取Username:"+username+"对应的用户信息");//若用户不存在,则可以抛出异常if("unknow".equals(username)){thrownewUnknownAccountException("用户不存在!");}//根据用户信息的情况,决定是否需要抛出其它异常if("monster".equals(username)){thrownewLockedAccountException("用户被锁定!");}/*根据用户信息的情况,构建AuthenticationInfo对象并返回,通常使用的实现类是SimpleAuthenticationInfo*以下信息是从数据库中获取的:*principal:认证的实体信息,可以是username,也可以是数据表对应的用户实体类对象*credentials:密码*realmName:当前realm对象的name,调用父类的getName()方法即可*/Objectprincipal=username;Objectcredentials="123456";StringrealmName=getName();SimpleAuthenticationInfoinfo=newSimpleAuthenticationInfo(principal,credentials,realmName);returninfo;}} 记得在Spring配置文件中拦截表单请求: <propertyname="filterChainDefinitions"><value>/login.jsp=anon<!--拦截表单请求-->/shiroLogin=anon<!--登出-->/logout=logout#everythingelserequiresauthentication:/**=authc</value></property> 登录成功后跳转至list.jsp: <%@pagecontentType="text/html;charset=UTF-8"language="java"%><html><head><title>Title</title></head><body><h4>ListPage</h4><ahref="logout">logout</a></body></html> 这里实现了一个登出请求,是因为Shiro在登录成功后会有缓存,此时无论用户名是否有效,都将成功登录,所以这里进行一个登出操作。 编写完成,最后启动项目即可。 在这里插入图片描述 若没有进行登录,将无法访问其它页面,若输入错误的用户名,则无法成功登录,也无法访问其它页面: 在这里插入图片描述 若输入正确的用户名和密码,则登录成功,可以访问其它页面: 在这里插入图片描述 重新来回顾一下上述的认证流程: 首先在login.jsp页面中有一个表单用于登录,当用户输入用户名和密码点击登录后,请求会被ShiroHandler控制器拦截 在ShiroHandler中校验用户是否已经被认证,若未认证,则将用户名和密码封装成UsernamePasswordToken对象,并执行登录 当执行登录后,UsernamePasswordToken对象会被传入ShiroRealm类的doGetAuthenticationInfo()方法的入参中,在该方法中对数据作进一步的校验 密码校验的过程 在刚才的例子中,我们实现了在用户登录前后对页面权限的控制,事实上,在程序中我们并没有去编写密码比对的代码,而登录逻辑显然对密码进行了校验,可以猜想这一定是Shiro帮助我们完成了密码的校验。 我们在UserNamePasswordToken类中的getPassword()方法中打一个断点: 在这里插入图片描述 此时以debug的方式启动项目,在表单中输入用户名和密码,点击登录,程序就可以在该方法处暂停运行: 在这里插入图片描述 我们往前找在哪执行了密码校验的逻辑,发现在doCredentialsMatch()方法: 在这里插入图片描述 再观察右边的参数: 在这里插入图片描述 这不正是我在表单输入的密码和数据表中查询出来的密码吗?由此确认在此处Shiro帮助我们对密码进行了校验。 在往前找找可以发现: 在这里插入图片描述 Shiro实际上是用CredentialsMatcher对密码进行校验的,那么为什么要大费周章地来找CredentialsMatcher呢? CredentialsMatcher是一个接口,我们来看看它的实现类: 在这里插入图片描述 那么相信大家已经知道接下来要做什么了,没错,密码的加密,而加密就是通过CredentialsMatcher来完成的。 MD5加密 加密算法其实有很多,这里以md5加密为例。 修改Spring配置文件中对自定义Realm的配置: <beanid="myRealm"class="com.wwj.shiro.realms.ShiroRealm"><propertyname="credentialsMatcher"><beanclass="org.apache.shiro.authc.credential.HashedCredentialsMatcher"><propertyname="hashAlgorithmName"value="MD5"/><!--指定加密次数--><propertyname="hashIterations"value="5"/></bean></property></bean> 这里因为Md5CredentialsMatcher类已经过期了,Shiro推荐直接使用HashedCredentialsMatcher。 这样配置以后,从表单中输入的密码就能够自动地进行MD5加密,但是从数据表中获取的密码仍然是明文状态,所以还需要对该密码进行MD5加密: publicstaticvoidmain(String[]args){StringalgorithmName="MD5";Objectcredentials="123456";Objectsalt=null;inthashIterations=5;Objectresult=newSimpleHash(algorithmName,credentials,salt,hashIterations);System.out.println(result);} 该代码可以参考Shiro底层实现,我们以Shiro同样的方式对其进行MD5加密,两份密码都加密完成了,以debug运行项目,再次找到Shiro校验密码的地方: 在这里插入图片描述 我在表单输入的密码是123456,经过校验发现,两份密码的密文是一致的,所以登录成功。 考虑密码重复的情况 刚才对密码进行了加密,进一步解决了密码的安全问题,但又有一个新问题摆在我们面前,倘若有两个用户的密码是一样的,这样即使进行了加密,因为密文是一样的,这样仍然会有安全问题,那么能不能够实现即使密码一样,但生成的密文却可以不一样呢? 当然是可以的,这里需要借助一个credentialsSalt属性(这里我们假设以用户名为标识进行密文的重新加密): publicstaticvoidmain(String[]args){StringalgorithmName="MD5";Objectcredentials="123456";Objectsalt=ByteSource.Util.bytes("aaa");//Objectsalt=ByteSource.Util.bytes("bbb");inthashIterations=5;Objectresult=newSimpleHash(algorithmName,credentials,salt,hashIterations);System.out.println(result);} 通过该方式,我们生成了两个不一样的密文,即使密码一样: c8b8a6de6e890dea8001712c9e1494963d12ecfbb349ddbe824730eb5e45deca 既然这里对加密进行了修改,那么在表单密码进行加密的时候我们也要进行修改: @OverrideprotectedAuthenticationInfodoGetAuthenticationInfo(AuthenticationTokenauthenticationToken)throwsAuthenticationException{//将参数转回UsernamePasswordTokenUsernamePasswordTokentoken=(UsernamePasswordToken)authenticationToken;//从UsernamePasswordToken中取出用户名Stringusername=token.getUsername();//调用数据库方法,从数据表中查询username对应的记录System.out.println("从数据库中获取Username:"+username+"对应的用户信息");//若用户不存在,则可以抛出异常if("unknow".equals(username)){thrownewUnknownAccountException("用户不存在!");}//根据用户信息的情况,决定是否需要抛出其它异常if("monster".equals(username)){thrownewLockedAccountException("用户被锁定!");}/*根据用户信息的情况,构建AuthenticationInfo对象并返回,通常使用的实现类是SimpleAuthenticationInfo*以下信息是从数据库中获取的:*principal:认证的实体信息,可以是username,也可以是数据表对应的用户实体类对象*credentials:密码*realmName:当前realm对象的name,调用父类的getName()方法即可*/Objectprincipal=username;Objectcredentials=null;//对用户名进行判断if("aaa".equals(username)){credentials="c8b8a6de6e890dea8001712c9e149496";}elseif("bbb".equals(username)){credentials="3d12ecfbb349ddbe824730eb5e45deca";}StringrealmName=getName();ByteSourcecredentialsSalt=ByteSource.Util.bytes(username);SimpleAuthenticationInfoinfo=newSimpleAuthenticationInfo(principal,credentials,credentialsSalt,realmName);returninfo;} 这样就轻松解决了密码重复的安全问题了。 多Relam的配置 刚才实现的是单个Relam的情况,下面来看看多个Relam之间的配置。 首先自定义第二个Relam: packagecom.wwj.shiro.realms;importorg.apache.shiro.authc.*;importorg.apache.shiro.crypto.hash.SimpleHash;importorg.apache.shiro.realm.AuthenticatingRealm;importorg.apache.shiro.util.ByteSource;publicclassShiroRealm2extendsAuthenticatingRealm{@OverrideprotectedAuthenticationInfodoGetAuthenticationInfo(AuthenticationTokenauthenticationToken)throwsAuthenticationException{System.out.println("ShiroRealm2...");UsernamePasswordTokentoken=(UsernamePasswordToken)authenticationToken;Stringusername=token.getUsername();System.out.println("从数据库中获取Username:"+username+"对应的用户信息");if("unknow".equals(username)){thrownewUnknownAccountException("用户不存在!");}if("monster".equals(username)){thrownewLockedAccountException("用户被锁定!");}Objectprincipal=username;Objectcredentials=null;if("aaa".equals(username)){credentials="ba89744a3717743bef169b120c052364621e6135";}elseif("bbb".equals(username)){credentials="29aa55fcb266eac35a6b9c1bd5eb30e41d4bfd8d";}StringrealmName=getName();ByteSourcecredentialsSalt=ByteSource.Util.bytes(username);SimpleAuthenticationInfoinfo=newSimpleAuthenticationInfo(principal,credentials,credentialsSalt,realmName);returninfo;}publicstaticvoidmain(String[]args){StringalgorithmName="SHA1";Objectcredentials="123456";Objectsalt=ByteSource.Util.bytes("bbb");inthashIterations=5;Objectresult=newSimpleHash(algorithmName,credentials,salt,hashIterations);System.out.println(result);}} 这里简单复制了第一个Relam的代码,并将加密方式改为了SHA1。 接下来修改Spring的配置文件: <?xmlversion="1.0"encoding="UTF-8"?><beansxmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsd"><beanid="securityManager"class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"><propertyname="cacheManager"ref="cacheManager"/><!--添加此处配置--><propertyname="authenticator"ref="authenticator"/></bean><beanid="cacheManager"class="org.apache.shiro.cache.ehcache.EhCacheManager"><propertyname="cacheManagerConfigFile"value="classpath:ehcache-shiro.xml"/></bean><!--添加此处配置--><beanid="authenticator"class="org.apache.shiro.authc.pam.ModularRealmAuthenticator"><propertyname="realms"><list><refbean="myRealm"/><refbean="myRealm2"/></list></property></bean><beanid="myRealm"class="com.wwj.shiro.realms.ShiroRealm"><propertyname="credentialsMatcher"><beanclass="org.apache.shiro.authc.credential.HashedCredentialsMatcher"><propertyname="hashAlgorithmName"value="MD5"/><propertyname="hashIterations"value="5"/></bean></property></bean><!--添加此处配置--><beanid="myRealm2"class="com.wwj.shiro.realms.ShiroRealm2"><propertyname="credentialsMatcher"><beanclass="org.apache.shiro.authc.credential.HashedCredentialsMatcher"><propertyname="hashAlgorithmName"value="SHA1"/><propertyname="hashIterations"value="5"/></bean></property></bean><beanid="lifecycleBeanPostProcessor"class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/><beanclass="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"depends-on="lifecycleBeanPostProcessor"/><beanclass="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"><propertyname="securityManager"ref="securityManager"/></bean><beanid="shiroFilter"class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"><propertyname="securityManager"ref="securityManager"/><propertyname="loginUrl"value="/login.jsp"/><propertyname="successUrl"value="/list.jsp"/><propertyname="unauthorizedUrl"value="/unauthorized.jsp"/><propertyname="filterChainDefinitions"><value>/login.jsp=anon/shiroLogin=anon/logout=logout#everythingelserequiresauthentication:/**=authc</value></property></bean></beans> 注释的地方就是需要修改的地方。 此时我们启动项目进行登录,查看控制台信息: 在这里插入图片描述 可以看到两个Relam都被调用了。 认证策略 既然有多个Relam,那么就一定会有认证策略的区别,比如多个Relam中是一个认证成功即为成功还是要所有Relam都认证成功才算成功,Shiro对此提供了三种策略: FirstSuccessfulStrategy:只要有一个Relam认证成功即可,只返回第一个Relam身份认证成功的认证信息,其它的忽略 AtLeastOneSuccessfulStrategy:只要有一个Relam认证成功即可,和FirstSuccessfulStrategy不同,它将返回所有Relam身份认证成功的认证信息 AllSuccessfulStrategy:所有Relam认证成功才算成功,且返回所有Relam身份认证成功的认证信息 默认使用的策略是AtLeastOneSuccessfulStrategy,具体可以通过查看源码来体会。 若要修改默认的认证策略,可以修改Spring的配置文件: <beanid="authenticator"class="org.apache.shiro.authc.pam.ModularRealmAuthenticator"><propertyname="realms"><list><refbean="myRealm"/><refbean="myRealm2"/></list></property><!--修改认证策略--><propertyname="authenticationStrategy"><beanclass="org.apache.shiro.authc.pam.AllSuccessfulStrategy"/></property></bean> 授权 授权也叫访问控制,即在应用中控制谁访问哪些资源,在授权中需要了解以下几个关键对象: 主体:访问应用的用户 资源:在应用中用户可以访问的url 权限:安全策略中的原子授权单位 角色:权限的集合 下面实现一个案例来感受一下授权的作用,新建aaa.jsp和bbb.jsp文件,并修改list.jsp: <%@pagecontentType="text/html;charset=UTF-8"language="java"%><html><head><title>Title</title></head><body><h4>ListPage</h4><ahref="aaa.jsp">aaaPage</a><br/><br/><ahref="bbb.jsp">bbbPage</a><br/><br/><ahref="logout">logout</a></body></html> 现在的情况是登录成功之后就能够访问aaa和bbb页面了: 在这里插入图片描述 但是我想实现这样一个效果,只有具备当前用户的权限才能够访问到指定页面,比如我以aaa用户的身份登录,那么我将只能访问aaa.jsp而无法访问bbb.jsp;同样地,若以bbb用户的身份登录,则只能访问bbb.jsp而无法访问aaa.jsp,该如何实现呢? 实现其实非常简单,修改Sping的配置文件: <propertyname="filterChainDefinitions"><value>/login.jsp=anon/shiroLogin=anon/logout=logout<!--添加角色过滤器-->/aaa.jsp=roles[aaa]/bbb.jsp=roles[bbb]#everythingelserequiresauthentication:/**=authc</value></property> 启动项目看看效果: 在这里插入图片描述 这里有一个坑,就是在编写授权之前,你需要将Relam的引用放到securityManager中: <beanid="securityManager"class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"><propertyname="cacheManager"ref="cacheManager"/><propertyname="authenticator"ref="authenticator"/><propertyname="realms"><list><refbean="myRealm"/><refbean="myRealm2"/></list></property></bean> 否则程序将无法正常运行。 现在虽然把权限加上了,但无论你是aaa用户还是bbb用户,你都无法访问到页面了,Shiro都自动跳转到了无权限页面,我们还需要做一些操作,对ShiroRelam类进行修改: packagecom.wwj.shiro.realms;importorg.apache.shiro.authc.*;importorg.apache.shiro.authz.AuthorizationInfo;importorg.apache.shiro.authz.SimpleAuthorizationInfo;importorg.apache.shiro.crypto.hash.SimpleHash;importorg.apache.shiro.realm.AuthorizingRealm;importorg.apache.shiro.subject.PrincipalCollection;importorg.apache.shiro.util.ByteSource;importjava.util.HashSet;importjava.util.Set;publicclassShiroRealmextendsAuthorizingRealm{@OverrideprotectedAuthenticationInfodoGetAuthenticationInfo(AuthenticationTokenauthenticationToken)throwsAuthenticationException{UsernamePasswordTokentoken=(UsernamePasswordToken)authenticationToken;Stringusername=token.getUsername();System.out.println("从数据库中获取Username:"+username+"对应的用户信息");if("unknow".equals(username)){thrownewUnknownAccountException("用户不存在!");}if("monster".equals(username)){thrownewLockedAccountException("用户被锁定!");}Objectprincipal=username;Objectcredentials=null;if("aaa".equals(username)){credentials="c8b8a6de6e890dea8001712c9e149496";}elseif("bbb".equals(username)){credentials="3d12ecfbb349ddbe824730eb5e45deca";}StringrealmName=getName();ByteSourcecredentialsSalt=ByteSource.Util.bytes(username);SimpleAuthenticationInfoinfo=newSimpleAuthenticationInfo(principal,credentials,credentialsSalt,realmName);returninfo;}/***授权时会被Shiro回调的方法*@paramprincipalCollection*@return*/@OverrideprotectedAuthorizationInfodoGetAuthorizationInfo(PrincipalCollectionprincipalCollection){//获取登录用户的信息Objectprincipal=principalCollection.getPrimaryPrincipal();//获取当前用户的角色Set<String>roles=newHashSet<>();roles.add("aaa");if("bbb".equals(principal)){roles.add("bbb");}//创建SimpleAuthorizationInfo,并设置其roles属性SimpleAuthorizationInfoinfo=newSimpleAuthorizationInfo(roles);returninfo;}} 首先将继承的类做了修改,改为继承AuthorizingRealm类,可以通过实现该类的doGetAuthenticationInfo()方法完成认证,通过doGetAuthorizationInfo()方法完成授权,所以源代码不用动,直接添加下面的doGetAuthorizationInfo()方法即可,看运行效果: 在这里插入图片描述 可以看到aaa用户只能访问到aaa.jsp而无法访问bbb.jsp,但是bbb用户却能够访问到两个页面,如果你仔细观察刚才添加的方法你就能够明白为什么。 @OverrideprotectedAuthorizationInfodoGetAuthorizationInfo(PrincipalCollectionprincipalCollection){//获取登录用户的信息Objectprincipal=principalCollection.getPrimaryPrincipal();//获取当前用户的角色Set<String>roles=newHashSet<>();roles.add("aaa");if("bbb".equals(principal)){roles.add("bbb");}//创建SimpleAuthorizationInfo,并设置其roles属性SimpleAuthorizationInfoinfo=newSimpleAuthorizationInfo(roles);returninfo;} 因为不管是什么用户登录,我都将aaa用户添加到了roles中,所以bbb用户是具有aaa用户权限的,权限完全是由你自己控制的,想怎么控制你就怎么写。 注解实现授权 先来看看关于授权的几个注解: @RequiresAuthentication:表示当前Subject已经通过login进行了身份验证;即 Subject. isAuthenticated()返回 true @RequiresUser:表示当前 Subject 已经身份验证或者通过记住我登录的 @RequiresGuest:表示当前Subject没有身份验证或通过记住我登录过,即是游客身份。 @RequiresRoles(value={“aaa”, “bbb”}, logical=Logical.AND):表示当前 Subject 需要角色aaa和bbb @RequiresPermissions (value={“user:a”, “user:b”},logical= Logical.OR):表示当前 Subject 需要权限user:a 或user:b 把Spring配置文件中的角色过滤器删掉,然后定义一个Service: packagecom.wwj.shiro.service;importorg.apache.shiro.authz.annotation.RequiresRoles;importorg.springframework.stereotype.Service;@ServicepublicclassShiroService{@RequiresRoles({"aaa"})publicvoidtest(){System.out.println("test...");}} 在test()方法上添加注解@RequiresRoles({"aaa"}),意思是该方法只有aaa用户才能访问,接下来在ShiroHandler中添加一个方法: @AutowiredprivateShiroServiceshiroService;@RequestMapping("/testAnnotation")publicStringtestAnnotation(){shiroService.test();return"redirect:/list.jsp";} 此时当你访问testAnnotation请求时,只有aaa用户能够成功访问,bbb用户就会抛出异常。 uiresRoles(value={“aaa”, “bbb”}, logical=Logical.AND):表示当前 Subject 需要角色aaa和bbb @RequiresPermissions (value={“user:a”, “user:b”},logical= Logical.OR):表示当前 Subject 需要权限user:a 或user:b 把Spring配置文件中的角色过滤器删掉,然后定义一个Service: packagecom.wwj.shiro.service;importorg.apache.shiro.authz.annotation.RequiresRoles;importorg.springframework.stereotype.Service;@ServicepublicclassShiroService{@RequiresRoles({"aaa"})publicvoidtest(){System.out.println("test...");}} 在test()方法上添加注解@RequiresRoles({"aaa"}),意思是该方法只有aaa用户才能访问,接下来在ShiroHandler中添加一个方法: @AutowiredprivateShiroServiceshiroService;@RequestMapping("/testAnnotation")publicStringtestAnnotation(){shiroService.test();return"redirect:/list.jsp";} 此时当你访问testAnnotation请求时,只有aaa用户能够成功访问,bbb用户就会抛出异常。 本文分享自微信公众号 - 码视界(otc_18679428729)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

优秀的个人博客,低调大师

10分钟彻底搞懂单页面应用路由

上一次,跟大家科普了小程序的自定义路由routes,开启了路由之旅;今天,顺势就单页面应用路由,跟大家唠个五毛钱,如果唠得不好……退…一块钱? 单页面应用特征 假设: 在一个 web 页面中,有1个按钮,点击可跳转到站内其他页面。 多页面应用: 点击按钮,会从新加载一个html资源,刷新整个页面; 单页面应用: 点击按钮,没有新的html请求,只发生局部刷新,能营造出一种接近原生的体验,如丝般顺滑。 SPA 单页面应用为什么可以几乎无刷新呢?因为它的SP——single-page。在第一次进入应用时,即返回了唯一的html页面和它的公共静态资源,后续的所谓“跳转”,都不再从服务端拿html文件,只是DOM的替换操作,是模(jia)拟(zhuang)的。 那么js又是怎么捕捉到组件切换的时机,并且无刷新变更浏览器url呢?靠hash和HTML5History。 hash 路由 特征 类似www.xiaoming.html#bar 就是哈希路由,当 # 后面的哈希值发生变化时,不会向服务器请求数据,可以通过 hashchange 事件来监听到 URL 的变化,从而进行DOM操作来模拟页面跳转 不需要服务端配合 对 SEO 不友好 原理 HTML5History 路由 特征 History 模式是 HTML5 新推出的功能,比之 hash 路由的方式直观,长成类似这个样子www.xiaoming.html/bar ,模拟页面跳转是通过 history.pushState(state, title, url) 来更新浏览器路由,路由变化时监听 popstate 事件来操作DOM 需要后端配合,进行重定向 对 SEO 相对友好 原理 vue-router 源码解读 以 Vue 的路由vue-router为例,我们一起来撸一把它的源码。 Tips:因为,本篇的重点在于讲解单页面路由的两种模式,所以,下面只列举了一些关键代码,主要讲解: 注册插件 VueRouter的构造函数,区分路由模式 全局注册组件 hash / HTML5History模式的 push 和监听方法 transitionTo 方法 注册插件 首先,作为一个插件,要有暴露一个install方法的自觉,给Vue爸爸去 use。 源码的install.js文件中,定义了注册安装插件的方法install,给每个组件的钩子函数混入方法,并在beforeCreate钩子执行时初始化路由: Vue.mixin({ beforeCreate () { if (isDef(this.$options.router)) { this._routerRoot = this this._router = this.$options.router this._router.init(this) Vue.util.defineReactive(this, '_route', this._router.history.current) } else { this._routerRoot = (this.$parent && this.$parent._routerRoot) || this } registerInstance(this, this) }, // 全文中以...来表示省略的方法 ... }); 区分mode 然后,我们从index.js找到整个插件的基类 VueRouter,不难看出,它是在constructor中,根据不同mode 采用不同路由实例的。 ... import {install} from './install'; import {HashHistory} from './history/hash'; import {HTML5History} from './history/html5'; ... export default class VueRouter { static install: () => void; constructor (options: RouterOptions = {}) { if (this.fallback) { mode = 'hash' } if (!inBrowser) { mode = 'abstract' } this.mode = mode switch (mode) { case 'history': this.history = new HTML5History(this, options.base) break case 'hash': this.history = new HashHistory(this, options.base, this.fallback) break case 'abstract': this.history = new AbstractHistory(this, options.base) break default: if (process.env.NODE_ENV !== 'production') { assert(false, `invalid mode: ${mode}`) } } } } 全局注册router-link组件 这个时候,我们也许会问:使用 vue-router 时, 常见的<router-link/>、 <router-view/>又是在哪里引入的呢? 回到install.js文件,它引入并全局注册了 router-view、router-link组件: import View from './components/view'; import Link from './components/link'; ... Vue.component('RouterView', View); Vue.component('RouterLink', Link); 在 ./components/link.js 中,<router-link/>组件上默认绑定了click事件,点击触发handler方法进行相应的路由操作。 const handler = e => { if (guardEvent(e)) { if (this.replace) { router.replace(location, noop) } else { router.push(location, noop) } } }; 就像最开始提到的,VueRouter构造函数中对不同mode初始化了不同模式的 History 实例,因而router.replace、router.push的方式也不尽相同。接下来,我们分别扒拉下这两个模式的源码。 hash模式 history/hash.js 文件中,定义了HashHistory 类,这货继承自 history/base.js 的 History 基类。 它的prototype上定义了push方法:在支持 HTML5History 模式的浏览器环境中(supportsPushState为 true),调用history.pushState来改变浏览器地址;其他浏览器环境中,则会直接用location.hash = path 来替换成新的 hash 地址。 其实最开始读到这里是有些疑问的,既然已经是 hash 模式为何还要判断supportsPushState?是为了支持scrollBehavior,history.pushState可以传参key过去,这样每个url历史都有一个key,用 key 保存了每个路由的位置信息。 同时,原型上绑定的setupListeners 方法,负责监听 hash 变更的时机:在支持 HTML5History 模式的浏览器环境中,监听popstate事件;而其他浏览器中,则监听hashchange。监听到变化后,触发handleRoutingEvent 方法,调用父类的transitionTo跳转逻辑,进行 DOM 的替换操作。 import { pushState, replaceState, supportsPushState } from '../util/push-state' ... export class HashHistory extends History { setupListeners () { ... const handleRoutingEvent = () => { const current = this.current if (!ensureSlash()) { return } // transitionTo调用的父类History下的跳转方法,跳转后路径会进行hash化 this.transitionTo(getHash(), route => { if (supportsScroll) { handleScroll(this.router, route, current, true) } if (!supportsPushState) { replaceHash(route.fullPath) } }) } const eventType = supportsPushState ? 'popstate' : 'hashchange' window.addEventListener( eventType, handleRoutingEvent ) this.listeners.push(() => { window.removeEventListener(eventType, handleRoutingEvent) }) } push (location: RawLocation, onComplete?: Function, onAbort?: Function) { const { current: fromRoute } = this this.transitionTo( location, route => { pushHash(route.fullPath) handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort ) } } ... // 处理传入path成hash形式的URL function getUrl (path) { const href = window.location.href const i = href.indexOf('#') const base = i >= 0 ? href.slice(0, i) : href return `${base}#${path}` } ... // 替换hash function pushHash (path) { if (supportsPushState) { pushState(getUrl(path)) } else { window.location.hash = path } } // util/push-state.js文件中的方法 export const supportsPushState = inBrowser && (function () { const ua = window.navigator.userAgent if ( (ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) && ua.indexOf('Mobile Safari') !== -1 && ua.indexOf('Chrome') === -1 && ua.indexOf('Windows Phone') === -1 ) { return false } return window.history && typeof window.history.pushState === 'function' })() HTML5History模式 类似的,HTML5History 类定义在 history/html5.js 中。 定义push原型方法,调用history.pusheState修改浏览器的路径。 与此同时,原型setupListeners 方法对popstate进行了事件监听,适时做 DOM 替换。 import {pushState, replaceState, supportsPushState} from '../util/push-state'; ... export class HTML5History extends History { setupListeners () { const handleRoutingEvent = () => { const current = this.current; const location = getLocation(this.base); if (this.current === START && location === this._startLocation) { return } this.transitionTo(location, route => { if (supportsScroll) { handleScroll(router, route, current, true) } }) } window.addEventListener('popstate', handleRoutingEvent) this.listeners.push(() => { window.removeEventListener('popstate', handleRoutingEvent) }) } push (location: RawLocation, onComplete?: Function, onAbort?: Function) { const { current: fromRoute } = this this.transitionTo(location, route => { pushState(cleanPath(this.base + route.fullPath)) handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort) } } ... // util/push-state.js文件中的方法 export function pushState (url?: string, replace?: boolean) { saveScrollPosition() const history = window.history try { if (replace) { const stateCopy = extend({}, history.state) stateCopy.key = getStateKey() history.replaceState(stateCopy, '', url) } else { history.pushState({ key: setStateKey(genStateKey()) }, '', url) } } catch (e) { window.location[replace ? 'replace' : 'assign'](url) } } transitionTo 处理路由变更逻辑 上面提到的两种路由模式,都在监听时触发了this.transitionTo,这到底是个啥呢?它其实是定义在 history/base.js 基类上的原型方法,用来处理路由的变更逻辑。 先通过const route = this.router.match(location, this.current)对传入的值与当前值进行对比,返回相应的路由对象;接着判断新路由是否与当前路由相同,相同的话直接返回;不相同,则在this.confirmTransition中执行回调更新路由对象,并对视图相关DOM进行替换操作。 export class History { ... transitionTo ( location: RawLocation, onComplete?: Function, onAbort?: Function ) { const route = this.router.match(location, this.current) this.confirmTransition( route, () => { const prev = this.current this.updateRoute(route) onComplete && onComplete(route) this.ensureURL() this.router.afterHooks.forEach(hook => { hook && hook(route, prev) }) if (!this.ready) { this.ready = true this.readyCbs.forEach(cb => { cb(route) }) } }, err => { if (onAbort) { onAbort(err) } if (err && !this.ready) { this.ready = true // https://github.com/vuejs/vue-router/issues/3225 if (!isRouterError(err, NavigationFailureType.redirected)) { this.readyErrorCbs.forEach(cb => { cb(err) }) } else { this.readyCbs.forEach(cb => { cb(route) }) } } } ) } ... } 最后 好啦,以上就是单页面路由的一些小知识,希望我们能一起从入门到永不放弃~~

优秀的个人博客,低调大师

【远程办公】5分钟学会搭建内网穿透

疫情期间大家都在讨论 远程办公的实现,推荐看下黄东旭大佬 写的 《PingCAP的5年远程办公实践》,以下内网映射工具作为常用补充。 来讲讲为啥要做内网映射 从公网中访问自己的内网设备一直是个麻烦事情,尤其是做微信开发等。设备可能处于路由器后,或者运营商因为IP地址短缺不给你分配公网IP地址。如果我们想直接访问到这些设备,一般非常麻烦。 求网管大佬在路由器上给自己内网加个端口映射 购买 花生壳 等动态域名解析软件 使用 natapp 等免费(也有付费的)的提供的内网映射服务 基于ngrok/frp自建内网映射服务 为什么放弃 ngrok,使用 frp 我们在2016年提供了一个ngrok 的免费服务,并且分享了搭建的步骤可以参考《Angrok 一个内网穿透服务》 ,搭建步骤对于一般的用户非常不友好,后边也就停止了相关的服务转向了 frp。 Github 的关注度对比 穿透协议支持 frp 支持 http ssh tcp udp ftp 等协议 开始动手 准备工作 搭建一个完整的frp服务,我们需要 公网IP 的 ECS 一台 域名 (若不需要解析则不需要) 安装 frp (frps)服务端 下载 frp 安装包 https://github.com/fatedier/frp/releases/download/v0.31.1/frp_0.31.1_darwin_amd64.tar.gz 解压压缩包,修改 frps.ini [common] bind_port = 7000 # frps 服务启动,占用的端口 vhost_http_port = 80 # frps 服务监听转发的端口 启动 frps 服务 ./frps -c ./frps.ini 安装 frp(frpc)客户端 在目标内网设备机器,安装客户端。 根据操作系统下载不同版本 自定义域名访问内网服务 修改 frpc.ini [common] server_addr = ECS的公网IP server_port = 7000 [随意但必须唯一] type = http local_port = 本地目标服务的端口 custom_domains = 自定义的域名 启动客户端 ./frpc -c ./frpc.ini 访问 自定义域名即可访问内网的服务 使用ssh访问公司内网机器 修改 frpc.ini [common] server_port = 7000 [随意但必须唯一] type = tcp local_ip = 127.0.0.1 local_port = 22 remote_port = 10022 启动客户端 ./frpc -c ./frpc.ini 通过 ssh 访问内网机器 ssh -p 10022 root@x.x.x.x

优秀的个人博客,低调大师

用 Authing 10分钟实现单点登录(SSO)

单点登录(Single Sign On),简称为SSO,是目前比较流行的企业业务整合的解决方案之一。 SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。 实现单点登录 开始之前 如果你不了解用户池、单点登录和认证授权,建议先阅读基础概念。 预备知识 基本的 HTML 和 CSS 知识 中级 JavaScript 技能 所需工具 你喜欢的文本编辑器 可以在本地运行的 Web 服务器(比如:npm install http-server -g) 注册一个 Authing 账号 如果你还没有账号,请点击这里注册 Authing 账号,注册完成后请进入控制台并创建一个用户池。 创建一个 OIDC 应用 第三方登录 -> OIDC 应用选项卡,点击蓝色的「创建 OIDC 应用」按钮。 填上你的应用名,指定此 OIDC 应用的二级域名(认证地址),回调地址,其他参数保留默认即可。点击「确定」。 参数解释 认证地址,一个 authing.cn 的二级域名,用户将在此网址进行登录。 回调 URL,OIDC 登录成功后,回调到开发者自己业务的地址。本教程为演示,填写的地址是 http://localhost:8080,实际场景下要填写自己的业务地址。 在应用列表中点击刚创建好的应用,记录下 AppID,二级域名,供以后使用。 使用 AuthingSSO SDK 集成单点登录 创建一个空白的 HTML 文档用来编写 Authing 程序 本教程只是为了演示,因此我们没选择高级框架,这可以让我们专注于 Authing 本身。 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" /> <title>Authing SSO Example</title> </head> <body></body> </html> 添加三个按钮 增加三个按钮控件到 body 中,目的是为了演示如何使用 SDK 管理单点登录状态。 <button id="btn-login">login</button> <button id="btn-track-session">trackSession</button> <button id="btn-logout">logout</button> 引入 AuthingSSO 并初始化 从 CDN 加载 AuthingSSO 的 SDK。填入你的 OIDC 应用 ID 和 域名,进行初始化。 <script src="https://cdn.jsdelivr.net/npm/@authing/sso/dist/AuthingSSO.umd.min.js"></script> <script> let auth = new AuthingSSO({ appId: "YOUR_OIDC_APP_ID", appType: "oidc", appDomain: "OIDC_APP_DOMAIN.authing.cn" }); </script> 为按钮注册点击事件 达到的效果是: 点击 login 按钮,浏览器会跳转到 OIDC 登录页面,与用户完成身份确认。 点击 trackSession 按钮,会显示当前登录状态。 点击 logout 按钮,进行单点登出。 let login = document.getElementById("btn-login"); let trackSession = document.getElementById("btn-track-session"); let logout = document.getElementById("btn-logout"); login.onclick = function() { auth.login(); }; trackSession.onclick = async function() { let res = await auth.trackSession(); alert(JSON.stringify(res)); }; logout.onclick = async function() { let res = await auth.logout(); alert(JSON.stringify(res)); }; 完整代码 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" /> <title>Authing SSO Example</title> </head> <body> <button id="btn-login">login</button> <button id="btn-track-session">trackSession</button> <button id="btn-logout">logout</button> <script src="https://cdn.jsdelivr.net/npm/@authing/sso/dist/AuthingSSO.umd.min.js"></script> <script> let auth = new AuthingSSO({ appId: "YOUR_OIDC_APP_ID", appType: "oidc", appDomain: "OIDC_APP_DOMAIN.authing.cn" }); let login = document.getElementById("btn-login"); let trackSession = document.getElementById("btn-track-session"); let logout = document.getElementById("btn-logout"); login.onclick = function() { auth.login(); }; trackSession.onclick = async function() { let res = await auth.trackSession(); alert(JSON.stringify(res)); }; logout.onclick = async function() { let res = await auth.logout(); alert(JSON.stringify(res)); }; </script> </body> </html> 示例代码可从 Github 上找到,建议将 Github 上的代码下载运行。 运行方法 在终端中运行以下命令 $ git clone https://github.com/Authing/authing-sso-demo $ cd authing-sso-demo $ npm install -g http-server $ http-server 之后在浏览器访问 http://localhost:8080。 {% hint style="warning" %} 如果本地 8080 端口已被占用,应用可能会运行在 8081、8082 等后续端口。 {% endhint %} 运行效果 最初,我们没有登录,因此,点击 trackSession 按钮获取到的登录状态为空。 现在我们点击 login 按钮,会跳转到 OIDC 应用的用户认证页面,输入用户名密码进行登录。 浏览器被重定向到我们之前设置的回调链接,记下 code 参数,用于后面换取用户信息。 点击 trackSession 按钮,此时能够获取到该用户的登录状态,包括用户 ID,应用 ID,应用类型。 点击 logout 按钮,输出单点登出成功。 此时我们再点击 trackSession 按钮,可见登录状态为空,说明用户已经单点登出了。 获取用户信息 使用 OIDC 流程中返回的 code 换取 access_token 向以下地址发送 POST 请求: POST https://OIDC_APP_DOMAIN.authing.cn/oauth/oidc/token body 参数 参数名 意义 client_id OIDC 应用的 app_id redirect_uri 在控制台配置的 OIDC 回调 url 其中的一个值 scope 需要请求的权限,如果需要获取 email 和手机号需要有 phone email,如果需要 refresh_token 需要包含 offline_access 参考 scope 表格 response_type OIDC 模式,可以为 code, id_token, id_token token, code id_token, code token, code id_token token 参考 OIDC 规范 prompt 可以为 none,login,consent 或 select_account,指定 AP 与 End-User 的交互方式,如需 refresh_token,必须为 consent 参考 OIDC 规范 state 一个随机字符串,用于防范 CSRF 攻击,如果 response 中的 state 值和发送请求之前设置的 state 值不同,说明受到攻击 nonce 一个随机字符串,用于防范 Replay 攻击 返回示例 { "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InIxTGtiQm8zOTI1UmIyWkZGckt5VTNNVmV4OVQyODE3S3gwdmJpNmlfS2MifQ.eyJqdGkiOiJ4R01uczd5cmNFckxiakNRVW9US1MiLCJzdWIiOiI1YzlmNzVjN2NjZjg3YjA1YTkyMWU5YjAiLCJpc3MiOiJodHRwczovL2F1dGhpbmcuY24iLCJpYXQiOjE1NTQ1Mzc4NjksImV4cCI6MTU1NDU0MTQ2OSwic2NvcGUiOiJvcGVuaWQgcHJvZmlsZSBvZmZsaW5lX2FjY2VzcyBwaG9uZSBlbWFpbCIsImF1ZCI6IjVjYTc2NWUzOTMxOTRkNTg5MWRiMTkyNyJ9.wX05OAgYuXeYM7zCxhrkvTO_taqxrCTG_L2ImDmQjMml6E3GXjYA9EFK0NfWquUI2mdSMAqohX-ndffN0fa5cChdcMJEm3XS9tt6-_zzhoOojK-q9MHF7huZg4O1587xhSofxs-KS7BeYxEHKn_10tAkjEIo9QtYUE7zD7JXwGUsvfMMjOqEVW6KuY3ZOmIq_ncKlB4jvbdrduxy1pbky_kvzHWlE9El_N5qveQXyuvNZVMSIEpw8_y5iSxPxKfrVwGY7hBaF40Oph-d2PO7AzKvxEVMamzLvMGBMaRAP_WttBPAUSqTU5uMXwMafryhGdIcQVsDPcGNgMX6E1jzLA", "expires_in": 3600, "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InIxTGtiQm8zOTI1UmIyWkZGckt5VTNNVmV4OVQyODE3S3gwdmJpNmlfS2MifQ.eyJzdWIiOiI1YzlmNzVjN2NjZjg3YjA1YTkyMWU5YjAiLCJub25jZSI6IjIyMTIxIiwiYXRfaGFzaCI6Ik5kbW9iZVBZOEFFaWQ2T216MzIyOXciLCJzaWQiOiI1ODM2NzllNC1lYWM5LTRjNDEtOGQxMS1jZWFkMmE5OWQzZWIiLCJhdWQiOiI1Y2E3NjVlMzkzMTk0ZDU4OTFkYjE5MjciLCJleHAiOjE1NTQ1NDE0NjksImlhdCI6MTU1NDUzNzg2OSwiaXNzIjoiaHR0cHM6Ly9hdXRoaW5nLmNuIn0.IQi5FRHO756e_eAmdAs3OnFMU7QuP-XtrbwCZC1gJntevYJTltEg1CLkG7eVhdi_g5MJV1c0pNZ_xHmwS0R-E4lAXcc1QveYKptnMroKpBWs5mXwoOiqbrjKEmLMaPgRzCOdLiSdoZuQNw_z-gVhFiMNxI055TyFJdXTNtExt1O3KmwqanPNUi6XyW43bUl29v_kAvKgiOB28f3I0fB4EsiZjxp1uxHQBaDeBMSPaRVWQJcIjAJ9JLgkaDt1j7HZ2a1daWZ4HPzifDuDfi6_Ob1ZL40tWEC7xdxHlCEWJ4pUIsDjvScdQsez9aV_xMwumw3X4tgUIxFOCNVEvr73Fg", "refresh_token": "WPsGJbvpBjqXz6IJIr1UHKyrdVF", "scope": "openid profile offline_access phone email", "token_type": "Bearer" } 验证 access_token 和 id_token 的合法性 OIDC 默认使用 OIDC 应用的 secret 对 token 进行验证(也就是在创建应用时默认选择 HS256 算法)。 如果你使用 javascript 那么可以使用 jsonwebtoken 进行验证: const jwt = require('jsonwebtoken'); let decoded = jwt.verify(token, <appSecret>); 如果是其他语言,那么你在服务端需要用 app_secret 作为 HS256 签名参数来计算签名和 JWT 中的签名进行对比,伪代码如下: HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), "1133fd20c14e4cc29b6ecb71fb8eb952"// app_secret ) 如果是 RS256 等非对称加密算法,需要使用公钥验证签名。Authing 将使用私钥进行签名,请使用 Authing 的公钥来验证签名: -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxRijj2seoesv5K0Z+ymR K7DSDPxdsM2sGQD2ZVhLjLsxZWJtXUXh7ERdUU6OT3BqYZZf7CLIhN6yyNtTOgfg pLG9HVJd7ZSKzuy2dS7mo8jD8YRtptAJmNFqw6z8tQp5MNG1ZHqp9isKqJmx/CFY kRdXBmjjj8PMVSP757pkC3jCq7fsi0drSSg4lIxrSsGzL0++Ra9Du71Qe/ODQKU0 brxaI1OKILtfcVPTHTaheV+0dw4eYkSDtyaLBG3jqsQbdncNg8PCEWchNzdO6aaj Uq4wbOzy/Ctp399mz0SGKfuC5S8gqAFABFT3DH3UD21ZztQZwFEV2AlvF+bcGEst cwIDAQAB -----END PUBLIC KEY----- 使用 access_token 换取用户信息 开发者在自己的服务中可以使用 access_token 换取用户信息。根据 scope 的不同,这里的返回信息也会有所不同,字段符合 OIDC 规范,字段解释请参考用户信息字段含义。 请求链接: GET https://users.authing.cn/oauth/oidc/user/userinfo?access_token=<access_token> 返回示例: { "sub": "<用户在 Authing 的唯一标识>", "nickname": "Authing", "name": "张三", "locale": "en-US" } 更多字段解释请参考用户信息字段含义。 接下来你可能还需要 了解 OIDC 协议: 理解OIDC流程 控制台是你管理所有 Authing 资源的地方,了解 Authing 控制台各模块包含的内容和你可以在控制台中做的事情: 控制台概览 了解 Authing 提供的多种部署模型,以帮助你选择该以怎样的形式部署 Authing: 部署模型 什么是 Authing? Authing 提供专业的身份认证和授权服务。 我们为开发者和企业提供用以保证应用程序安全所需的认证模块,这让开发人员无需成为安全专家。 你可以将任意平台的应用接入到 Authing(无论是新开发的应用还是老应用都可以),同时你还可以自定义应用程序的登录方式(如:邮箱/密码、短信/验证码、扫码登录等)。 你可以根据你使用的技术,来选择我们的 SDK 或调用相关 API 来接入你的应用。当用户发起授权请求时,Authing 会帮助你认证他们的身份和返回必要的用户信息到你的应用中。 官网:http://authing.cn 小登录:https://wxapp.authing.cn/#/ 仓库:欢迎star,欢迎pr https://gitee.com/Authi_ng https://github.com/authing Demo: https://sample.authing.cn https://github.com/Authing/qrcode-sample 文档:https://docs.authing.cn/authing/

优秀的个人博客,低调大师

3分钟,了解阿里云热门产品 ZooKeeper

本文来自畅销书《从 Paxos 到 Zookeeper》作者倪超,讲述了其对阿里云最新发布的微服务引擎 MSE 产品的使用初体验。 (图片来自《从 Paxos 到 ZooKeeper 》一书的封面) 屏幕前的读者朋友们 如果 正在为运维 ZooKeeper 这份至关重要,但繁琐而枯燥 甚至心里没底的活 而烦恼 如果 正在为自己的应用 找寻一个高可用,且免运维的 ZooKeeper 那么,它来了 阿里云全托管的 ZooKeeper 集群产品 微服务引擎 MSE 读者可以在任何搜索引擎搜索“微服务引擎 MSE”,即可直达产品官网。 阿里巴巴和 ZooKeeper 的结缘 从微服务引擎 MSE 的官方网站上,了解到,自 2010 年左右第一次引入以来,Apache ZooKeeper 目前在阿里巴巴集团内部已经有了将近 10 年的发展,使用的场景非常广泛,基于 ZooK

优秀的个人博客,低调大师

Eclipse教程1 - 3分钟理清Maven依赖

作者: 韩勇 日期: 2018-12-02 前言 大学学习 Java 时开始接触 Eclipse,喜欢其强大和方便,并立志成为一名 Java 开发。然而近几年 Eclipse 的流行度开始下滑,一方面有 Eclipse 自身的不足(安装插件需要科学上网,新版卡顿、报错),一方面我在想是不是 Eclipse 更适合我这样熟练使用 Eclipse 的用户,而不是普通 Java 开发。我觉得我应该写一些教程,帮助大家了解和使用 Eclipse,作为对 Eclipse 社区的回馈。 安装 Eclipse 首先需要安装 JDK,Linux(或 Cygwin)下可使用 sdkman 安装(需要科学上网)。 sdk install java 8.0.191-oracle 下载解压 Eclipse Java 开发软件包,可从国内镜像加速下载,如

优秀的个人博客,低调大师

5分钟掌握var,let和const异同

转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具、解决方案和服务,赋能开发者。 原文出处:https://dzone.com/articles/javascript-difference-between-var-let-and-const-ke 这个话题对于一些老鸟来说可能根本算不上疑问,但对于新手来说也许除了最常见的var之外,let和const较少使用的机会。 所以在本文中,我将会通过一些例子来为大家介绍如何在Javascript中灵活使用var、let和const这些关键字来定义变量,以及这些关键字它们之间有什么异同。 如果你懒得看下面的代码,那我可以先把概念结论说出: var定义的变量可被更改,如果不初始化而直接使用也不会报错 let定义的变量和var类似,但作用域在当前声明的范围内 const定义的变量只可初始化一次且作用域内不可被更改,使用前必须初始化 下面我将通过示例的形式来详细为大家介绍它们三者之间的异同: Var var用以声明一个变量,并且同时我们可以在声明语句中初始化所声明的变量。 例如: var a = 10; 变量的声明,会在代码被执行之前被处理。 用var声明的JavaScript变量,其可用范围在当前执行上下文。 在函数外声明的JavaScript变量,其作用范围是全局。 考虑以下代码片段: function nodeSimplified() { var a =10; console.log(a); // 输出 10 if(true) { var a=20; console.log(a); // 输出 20 } console.log(a); // 输出 20 } 在上面的代码中,你可以发现,当变量a在if代码段里被更新时,它的值被全局更新了,因此在经过了if代码后,被更新的值仍然被保留着。这与其他语言中的全局变量有点类似。但是,在使用这个功能时要非常小心,因为它有可能会覆盖一个已有的值。 let let语句在一个块级范围里声明一个局部变量。和var类似,我们可以在声明时初始化它的值。 例如: let a = 10; 这个语句允许你创建一个变量,使它的作用范围被限制在它所在的代码块。 它和Java、C#等其他语言的变量类似。 考虑下面的代码片段: function nodeSimplified() { let a =10; console.log(a); // output 10 if(true) { let a=20; console.log(a); // output 20 } console.log(a); // output 10 } 它和大多数语言中我们所见的表现行为是一致的。 function nodeSimplified() { let a =10; let a =20; // 抛出语法错误 console.log(a); } 错误信息:"未捕获的异常:标识符'a'已经被声明过。"但如果使用var就没事: function nodeSimplified() { var a =10; var a =20; console.log(a); // 输出 20 } 使用let语句,可以很好的维护变量的作用范围。当使用内部函数时,let语句让你的代码更整洁。 我希望上面的例子能帮你更好地理解var和let。如果有任何疑问,请在评论区留言。 const const语言中的变量只能被赋值一次,然后就不能在被赋值。const语句的作用范围和let语句一样。 例如: const a = 10; function nodeSimplified() { const MY_VARIABLE =10; console.log(MY_VARIABLE); // 输出 10 } 照例,命名规范指出我们应该用大写字母声明常量。当然,const a = 10在上面的代码中会的起到相同的作用。为了让代码长期可维护,命名规范还是值得遵守的。 问题:如果给一个const变量重新赋值会发生什么?考虑下面的代码: function nodeSimplified() { const MY_VARIABLE =10; console.log(MY_VARIABLE); // 输出 10 MY_VARIABLE =20; // 抛出类型错误 console.log(MY_VARIABLE); } 错误信息:"未捕获的类型错误:给const变量赋值"。 当我们尝试给已有的const变量赋值时,这段代码会抛出一个错误。 介绍就到此结束,希望这篇短小精悍的文章能够帮助到各位更好的理解在Javascript中声明变量时使用不同关键字上到底有何异同。

资源下载

更多资源
腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册