JWT
jwt与传统session方式对比
基于session的方式会在服务器端产生一个session,然后通过jsessionid对比来找到用户对应的session,当session增多对服务器是一个很大的开销,而基于jwt的方式,每次客户端带来一个token直接通过解析token来鉴权 token可以存储在localstorage、sessionstorage、cookie,localstorage存于本地,如果不手动清楚不会被清掉;sessionstorage存于会话,当浏览器窗口关闭则清掉
创建jwt
导入依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
配置文件
audience.clientId=098f6bcd4621d373cade4e832627b4f6
audience.base64Secret=MDk4ZjZiY2Q0NjIxZDM3M2NhZGU0ZTgzMjYyN2I0ZjY=
audience.name=restapiuser
audience.expiresSecond=172800
audience实体
@Data
@ConfigurationProperties(prefix = "audience")
@Component
public class Audience {
private String clientId;
private String base64Secret;
private String name;
private int expiresSecond;
}
创建方法
public static String createJWT(String userId, Audience audience) {
String token = null;
try {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(audience.getBase64Secret());
Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
String encryId = Base64Util.encode(userId);
JwtBuilder builder = Jwts.builder().setHeaderParam("typ", "JWT")
.claim("userId", userId)
.setIssuer(audience.getClientId())
.setIssuedAt(new Date())
.setAudience(audience.getName())
.signWith(signatureAlgorithm, signingKey);
int TTLMillis = audience.getExpiresSecond();
if (TTLMillis >= 0) {
long expMillis = nowMillis + TTLMillis;
Date exp = new Date(expMillis);
builder.setExpiration(exp)
.setNotBefore(now);
}
token = builder.compact();
} catch (Exception e) {
log.error("签名失败", e);
}
return token;
}
jwt的校验
拦截器进行统一校验
拦截器在过滤器之后,可以发挥和过滤器相同的功能,但是过滤器是针对url进行过滤,而拦截器是针对请求的资源进行拦截,所以拦截器可以具体到处理器方法
public class JwtInterceptor implements HandlerInterceptor {
@Autowired
private Audience audience;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String token = request.getHeader("token");
if(token!=null){
if(JwtTokenUtil.parseJwt(token,audience.getBase64Secret())){
return true;
}else{
ObjectMapper mapper = new ObjectMapper();
String value = mapper.writeValueAsString("");
response.getWriter().write(value);
response.getWriter().close();
return false;
}
}else{
if(request.getRequestURI().endsWith("login")){
return true;
}else{
ObjectMapper mapper = new ObjectMapper();
String value = mapper.writeValueAsString("");
response.getWriter().write(value);
response.getWriter().close();
return false;
}
}
}
}
配置拦截器
@Configuration
public class DemoWebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins("*");
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JwtInterceptor());
}
}
解析jwt
public static boolean parseJwt(String jsonWebToken,String base64Security){
try {
Claims claims = Jwts.parser()
.setSigningKey(DatatypeConverter.parseBase64Binary(base64Security))
.parseClaimsJws(jsonWebToken).getBody();
return true;
} catch (ExpiredJwtException eje) {
log.error("===== Token过期 =====", eje);
return false;
} catch (Exception e){
log.error("===== token解析异常 =====", e);
return false;
}
}
Shiro
概述
Shiro是一个Java安全框架,它用于认证(authentication),授权(authorization),加密(cryptography),会话管理,适用于小型移动应用到大型企业级应用的各种应用
入门实例
配置文件
[users]
root = secret, admin
guest = guest, guest
presidentskroob = 12345, president
darkhelmet = ludicrousspeed, darklord, schwartz
lonestarr = vespa, goodguy, schwartz
[roles]
admin = *
schwartz = lightsaber:*
goodguy = winnebago:drive:eagle5
测试
@Test
public void test1(){
Factory<SecurityManager> factory = new IniSecurityManagerFactory();
SecurityManager manager = factory.getInstance();
SecurityUtils.setSecurityManager(manager);
Subject currentUser = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("root","secret");
try{
currentUser.login(token);
System.out.println(currentUser.getPrincipals());
} catch (UnknownAccountException uae){
System.out.println("账号不存在");
} catch (IncorrectCredentialsException ice){
System.out.println("密码不正确");
}
if(currentUser.hasRole("admin")){
System.out.println(currentUser.getPrincipals()+"有角色admin");
}else{
System.out.println(currentUser.getPrincipals()+"没有角色admin");
}
if(currentUser.isPermitted("lightsaber:perm")){
System.out.println(currentUser.getPrincipals()+"有权限perm");
}else{
System.out.println(currentUser.getPrincipals()+"没有权限perm");
}
}
核心概念
- Subject:Subject本质上是当前执行用户的安全特定的"视图",Subject不一定表示的是一个人,它表示任何当前与软件交互的对象,所有Subject都绑定到SecurityManager,与Subject交互的所有委托都会交给SecurityManager
- SecurityManager:SecurityManager是Shiro体系结构的核心,充当一种“保护伞”对象,协调内部安全组件,它管理着所有的Subject,类似SpringMVC中的前端控制器
- Realm:领域充当了Shiro和应用程序安全数据之间的“桥梁”或“连接器”。当需要与安全相关的数据(如用户帐户)进行实际交互以执行身份验证(登录)和授权(访问控制)时,SercurityManager会从域中寻找,它类似DataSource
- Shiro应用流程:应用代码通过Subject来进行认证和授权,Subject再将其委托给SercurityManager,事先我们再SercurityManager中注入Realm(Shiro不提供维护用户/权限,而是由开发人员自己注入),然后SercurityManager得到合法的用户及权限进行判断
核心组件
- Subject:主体,任何可以与应用交互的对象
- SercurityManager:相当于SpringMVC中的前端控制器,Shiro的核心,管理着所有的Subject,控制认证、授权、会话和缓存管理
- Authenticator:认证器,负责主体认证,可以自行扩展认证规范
- Authorizer:授权器,用来决定用户是否有权限进行相应的操作,控制资源是否能被访问
- Realm:数据域,可以有一个或多个,由用户自行实现,Shiro不知道用户和权限数据在哪里以及以什么方式存储,所以Realm一般由自己实现,如缓存实现、jdbc实现等
- SessionManager:会话管理器,用户管理Session的生命周期
- SessionDao:会话数据访问对象,可以将session保存到数据库
- CacheManager:缓存控制器,用来管理如权限等数据的缓存,这些数据一般很少变,所以放到缓存中提高性能
- Cryprography:密码模块,用于对密码的加密解密
SpringMVC整合Shiro
认证
- 添加依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.7.1</version>
</dependency>
- 自定义Realm
public class DbRealm extends AuthorizingRealm {
@Autowired
private UserDao userDao;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String account = (String) token.getPrincipal();
User user = userDao.getByAccount(account);
if(user==null){
throw new UnknownAccountException();
}
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user.getAccount(),user.getPassword(),"realm");
return info;
}
}
该realm作为SecurityManager和数据库之间的桥梁,将token中的认证信息与数据库中的信息进行认证,然后得到认证信息
- 编写配置文件shiro.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<bean id="dbrealm" class="com.rbac.shiro.DbRealm"></bean>
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="dbrealm"></property>
</bean>
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/login.jsp"/>
<property name="unauthorizedUrl" value="/unauthorized.jsp"/>
<property name="filterChainDefinitions">
<value>
/index.jsp = authc
/unauthorized.jsp = anon
/login.jsp = anon
/user/login = anon
/logout = logout
/** = user
</value>
</property>
</bean>
</beans>
shiro的配置文件可以直接配置在spring的配置文件中,但单独配置更方便维护,所以要在applicationContext.xml中将该配置文件引用<import resource="classpath:shiro.xml"/> 通过字段的形式,将Realm和SecurityManager绑定 loginUrl表示登录页面的url,当没有认证时自动跳转到指定的登录页面 filterChainDefinitions指定uri和过滤器的映射,这里的过滤器是shiro默认的过滤器
shiro默认过滤器
配置 | 对应的过滤器 | 功能 |
---|
anon | AnonymousFilter | 指定的url可以匿名访问(未登录不会被拦截) | authc | FormAuthenticationFilter | 需要登录才能访问,如果没有登录则跳转到登录页面 | logout | authc.LogoutFilter | 退出过滤器,主要属性redirectUrl为退出时重定向的地址,session将会失效 | user | UserFilter | 用户过滤器,当前请求存在经过身份验证的用户才可访问,登录操作不做检查 | authcBasic | BasicHttpAuthenticationFilter | 经过httpbaic验证的访问才能通过 | roles | RolesAuthorizationFilter | 验证用户是否有指定角色 | perms | PermissionsAuthorizationFilter | 验证用户是否有指定权限 | port | PortFilter | 端口过滤,表示访问可以通过的端口 |
加密/解密
- 配置凭证匹配器
<bean id="dbrealm" class="com.rbac.shiro.DbRealm">
<property name="credentialsMatcher" ref="credentialMatcher"/>
</bean>
<bean id="credentialMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="MD5"/>
<property name="hashIterations" value="1024"/>
</bean>
- 在数据域认证的时候加上盐值(这里使用用户账号作为盐值)
ByteSource salt = ByteSource.Util.bytes(account);
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user.getAccount(),user.getPassword(),salt,"realm");
return info;
授权
- 配置授权的realm
@Autowired
private PermissionDao permissionDao;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
List<Permission> permissionList = permissionDao.getByAccount((String) principals.getPrimaryPrincipal());
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
permissionList.forEach(p->{
info.addStringPermission(p.getIdentification());
});
return info;
}
realm的授权这一步的目的是为当前登录的用户赋予权限,即查到当前用户的权限并将这些权限放到授权信息中,shiro会拿这些权限来与当前访问需要的权限对比,如果当前访问需要的权限在这些权限中,则允许访问
- 配置url与权限的映射map
public class PermsMapFactoryBean implements FactoryBean<Map<String,String>> {
@Autowired
private PermissionDao permissionDao;
@Override
public Map<String, String> getObject() throws Exception {
List<Permission> permissionList = permissionDao.getByCondition(new Permission());
Map<String,String> map = new LinkedHashMap<>();
map.put("/index.jsp","authc");
map.put("/login.jsp","anon");
map.put("/user/login","anon");
map.put("/logout","logout");
map.put("/403.jsp","anon");
permissionList.forEach(p->{
map.put("/"+p.getLink(),"perms["+p.getIdentification()+"]");
});
map.put("/**","user");
return map;
}
@Override
public Class<?> getObjectType() {
return Map.class;
}
}
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/login.jsp"/>
<property name="unauthorizedUrl" value="/403.jsp"/>
<property name="filterChainDefinitionMap" ref="permsMap"/>
</bean>
<bean id="permsMap" class="com.rbac.shiro.PermsMapFactoryBean"/>
用户权限是动态数据,所以url和权限过滤器的映射不能写死在xml文件中,需要动态的添加映射,所以使用fileterChainDefinitionMap而非filterChainDefinit,自定义一个Bean,继承自FactoryBean来获得映射map
缓存(整合shiro ehcache)
- 导入依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.7.1</version>
</dependency>
- ehcache配置文件ehcache.xml
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<diskStore path="java.io.tmpdir/Tmp_EhCache"/>
<defaultCache
eternal="false"
maxElementsInMemory="10000"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="1800"
timeToLiveSeconds="259200"
memoryStoreEvictionPolicy="LRU"/>
</ehcache>
- 配置缓存管理器
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="dbrealm"></property>
<property name="cacheManager" ref="cacheManager"/>
</bean>
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/>
</bean>
配置缓存之后,只有第一次查找权限从数据库中查找,然后读取到内存中,之后每次直接从内存中取得
过滤器解析
理论基础
过滤器是servlet规范中提出的,所有过滤器都是最终实现Filter接口,所有请求想要通过过滤器要找的都是doFilter方法,shiro的默认过滤器自然也是这样,以这一理论点为基础,就可以清晰的分析处shiro的过滤器执行原理
执行原理(以FormAuthenticationFilter为例)
- OncePerRequestFilter实现了doFilter方法,作用是确保请求只经过一次过滤器,并在doFilter方法中调用了doFilterInternal方法,doFilterInternal方法由子类实现,
- AdviceFilter子类实现了doFilterInternal方法,在方法中调用了preHandle方法,postHandle方法,使用preHandle方法来判断是否继续执行,postHandle方法由另一子类重写,如果设置了HSTS(http严格安全传输协议)将其写入http头,否则不执行任何操作
- PathMatchingFilter子类实现了preHanle方法,使用isFilterChainContinued方法来判断是否继续,isFilterChainContinued使用onPreHandle方法来判断是否继续
- AccessContrllerFilter子类重写了onPreHandle方法
return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue); 当isAccessAllowed返回true时,onAccessDenied不执行;返回值即isAccessAllowed方法的返回值,当isAccessAllowed返回false时,返回值为onAccessDenied的返回值 - AuthenticationFilter子类实现了isAccessAllowed方法,该方法判断是否通过认证
(return subject.isAuthenticated() && subject.getPrincipal() != null; ) AuthenticationFilter子类也实习那了isAccessAllowed方法,该方法在判断是否通过认证的基础上,对于登录请求和拥有权限限定符的请求也会返回true
return super.isAccessAllowed(request, response, mappedValue) || (!isLoginRequest(request, response) && isPermissive(mappedValue)); - FormAuthenticationFilter子类实现了onAccessDenied方法,该方法对登录请求放行并对非登录请求记录请求并做出跳转到登录地址的响应
SpringBoot整合shiro
Springboot配置shiro
- 导入依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.7.1</version>
</dependency>
- 配置bean,与springmvc中的shiro.xml配置一样,这里也可以使用shiro.xml,但是官方推荐java的方式
@Configuration
public class ShiroConfig {
@Bean
public JwtRealm jwtRealm(){
return new JwtRealm();
}
@Bean
public DefaultWebSecurityManager securityManager(){
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(jwtRealm());
return manager;
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager());
shiroFilterFactoryBean.setFilterChainDefinitionMap(permsMapFactoryBean().getObject());
return shiroFilterFactoryBean;
}
@Bean
public PermsMapFactoryBean permsMapFactoryBean(){
return new PermsMapFactoryBean();
}
}
使用jwt替代shiro默认的认证
- 自定义过滤器实现jwt认证
public class JwtFilter extends AuthenticationFilter {
@Autowired
private Audience audience;
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
HttpServletRequest req = (HttpServletRequest) request;
String token = req.getHeader("token");
if(token==null) return false;
if(JwtTokenUtil.parseJwt(token,audience.getBase64Secret())){
return true;
}
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
System.out.println("认证失败");
return false;
}
}
- 将自定义的过滤器添加到过滤器链中
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager());
Map<String, Filter> filterMap = new HashMap<>();
filterMap.put("jwt",jwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
shiroFilterFactoryBean.setFilterChainDefinitionMap(permsMapFactoryBean().getObject());
return shiroFilterFactoryBean;
}
@Bean
public JwtFilter jwtFilter(){
return new JwtFilter();
}
使用jwt替代掉默认的user过滤器
@Override
public Map<String, String> getObject() {
Map<String,String> map = new LinkedHashMap<>();
map.put("/user/login","anon");
map.put("/logout","logout");
map.put("/**","jwt");
return map;
}
自定义shiro默认的授权
- 自定义授权过滤器
public class PermsFilter extends PermissionsAuthorizationFilter {
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException{
return false;
}
}
这里自定义的授权过滤器还是沿用了shiro原有的isAccessAllowed方法,这种情况下使用了jwt代替了原有的授权是不成功的,因为在isAccessAllowed方法对是否认证进行了判断,而没有执行shiro的认证的话判断是不成功的,所以对于登录请求还是要使用shiro认证来进行,即在UserController的login方法调用shiro的认证逻辑
- 配置授权过滤器
@Bean
public PermsFilter permsFilter(){
return permsFilter();
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager());
Map<String, Filter> filterMap = new HashMap<>();
filterMap.put("jwt",jwtFilter());
filterMap.put("perms",permsFilter());
shiroFilterFactoryBean.setFilters(filterMap);
shiroFilterFactoryBean.setFilterChainDefinitionMap(permsMapFactoryBean().getObject());
return shiroFilterFactoryBean;
}
注意
按照上述使用shiro原有的认证和授权的isAccessAllowed方法的话,还是用到了cookie和session,这样就不得不面临一个跨域问题,当发送跨域请求的时候如果没有带上cookie的话shiro会认为是不同用户的请求,这样就不能通过授权逻辑中的认证,解决办法: 1. 前端解决跨域问题,通过如apache、ngix这样的web服务器来发送请求 2. 重写isAccessAllowed方法
前端权限控制
问题引入
进行权限管理时,可以通过数据库直接绑定链接、按钮的形式使前端对无权限的功能不展示,但是这样不能阻止地址栏的直接访问
没有登录,访问任何功能直接跳转到登录
判断是否登录,根据判断是否存在token来确定 localStorage:一直存在于浏览器,必须手动清除 sessionStorage:浏览器关闭,数据自动清楚 cookie:根据指定的时间保持数据
导航守卫
如果要判断是否登录,在每一个跳转时都编写一次太麻烦,使用导航守卫就能很好的解决这一问题,全局前置守卫类似于servlet的Filter,能够在导航前进行过滤操作
全局前置守卫
const router = new VueRouter({ ... })
router.beforeEach((to, from, next) => {
})
参数
- to:即将要进入的路由对象,要跳转到哪里去
- from:当前正要离开的路由,从哪里跳转
- next:类似于过滤器中的doFilter方法,表示执行跳转
next():进行跳转 next(false):中断当前的跳转 next("/路径"):跳转到一个不同的地址 next(error):如果传入的参数是一个error,导航被终止并将错误交给router.onError回调
没有权限访问的页面,进行提示
对后台查到的权限链接动态增加路由,并使用一个通配符表示的路由添加在最后,这样当访问的时候访问的路由不存在前面可以访问的路由中,就会跳转到最后一个通配符表示的路由
|