基于session的方式会在服务器端产生一个session,然后通过jsessionid对比来找到用户对应的session,当session增多对服务器是一个很大的开销,而基于jwt的方式,每次客户端带来一个token直接通过解析token来鉴权
token可以存储在localstorage、sessionstorage、cookie,localstorage存于本地,如果不手动清楚不会被清掉;sessionstorage存于会话,当浏览器窗口关闭则清掉
导入依赖
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.1version>
dependency>
配置文件
##jwt配置
# 代表这个JWT的接收对象,存入audience
audience.clientId=098f6bcd4621d373cade4e832627b4f6
# 密钥, 经过Base64加密, 可自行替换
audience.base64Secret=MDk4ZjZiY2Q0NjIxZDM3M2NhZGU0ZTgzMjYyN2I0ZjY=
# JWT的签发主体,存入issuer
audience.name=restapiuser
# 过期时间,时间戳
audience.expiresSecond=172800
audience实体
@Data
@ConfigurationProperties(prefix = "audience")//获得属性文件中前缀为audience的属性来存入对应的属性中
@Component
public class Audience {
private String clientId;
private String base64Secret;
private String name;
private int expiresSecond;
}
创建方法
/**
* 构建jwt
* @param userId
* @param audience
* @return
*/
public static String createJWT(String userId, Audience audience) {
String token = null;
try {
// 使用HS256加密算法
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());
//userId是重要信息,进行加密下
String encryId = Base64Util.encode(userId);
//添加构成JWT的参数
JwtBuilder builder = Jwts.builder().setHeaderParam("typ", "JWT")
// 可以将基本不重要的对象信息放到claims
.claim("userId", userId)
.setIssuer(audience.getClientId()) // 代表这个JWT的签发主体;
.setIssuedAt(new Date()) // 是一个时间戳,代表这个JWT的签发时间;
.setAudience(audience.getName()) // 代表这个JWT的接收对象;
.signWith(signatureAlgorithm, signingKey);
//添加Token过期时间
int TTLMillis = audience.getExpiresSecond();
if (TTLMillis >= 0) {
long expMillis = nowMillis + TTLMillis;
Date exp = new Date(expMillis);
builder.setExpiration(exp) // 是一个时间戳,代表这个JWT的过期时间;
.setNotBefore(now); // 是一个时间戳,代表这个JWT生效的开始时间,意味着在这个时间之前验证JWT是会失败的
}
//生成JWT
token = builder.compact();
} catch (Exception e) {
log.error("签名失败", e);
//抛出创建失败的异常
}
return token;
}
拦截器在过滤器之后,可以发挥和过滤器相同的功能,但是过滤器是针对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转换json格式
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
/**
* 解析jwt
* @param jsonWebToken
* @param base64Security
* @return
*/
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是一个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");
}
}
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-springartifactId>
<version>1.7.1version>
dependency>
public class DbRealm extends AuthorizingRealm {
@Autowired
private UserDao userDao;
/**
* 授权
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
/**
* 认证
* @param token
* @return
* @throws AuthenticationException
*/
@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中的认证信息与数据库中的信息进行认证,然后得到认证信息
<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中将该配置文件引用
通过字段的形式,将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;
@Autowired
private PermissionDao permissionDao;
/**
* 授权,查到当前用户的权限并授予当前用户
* @param principals
* @return 授权信息
*/
@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会拿这些权限来与当前访问需要的权限对比,如果当前访问需要的权限在这些权限中,则允许访问
/***
* @author shaofan
* @Description url与过滤器链映射
* @Date 2021-8-3
* @Time 17:56
*/
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<>();//过滤器映射的配置顺序需要指定,使用hashmap则是随机顺序
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
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-ehcacheartifactId>
<version>1.7.1version>
dependency>
<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的过滤器执行原理
return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
return subject.isAuthenticated() && subject.getPrincipal() != null;
)return super.isAccessAllowed(request, response, mappedValue) || (!isLoginRequest(request, response) && isPermissive(mappedValue));
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-spring-boot-web-starterartifactId>
<version>1.7.1version>
dependency>
@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();
}
}
public class JwtFilter extends AuthenticationFilter {
@Autowired
private Audience audience;
/**
* 判断是否通过认证
* @param request
* @param response
* @param mappedValue
* @return
*/
@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;
}
/**
* 认证失败之后调用
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
// if(登录请求)
// return true;
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<>();//过滤器映射的配置顺序需要指定,使用hashmap则是随机顺序
map.put("/user/login","anon");
map.put("/logout","logout");
map.put("/**","jwt");
return map;
}
public class PermsFilter extends PermissionsAuthorizationFilter {
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException{
//自定义没有权限时候的逻辑,默认是重定向到指定的页面,但在前后端分离开发时,这里逻辑应返回一个json数据
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) => {
})
next():进行跳转
next(false):中断当前的跳转
next("/路径"):跳转到一个不同的地址
next(error):如果传入的参数是一个error,导航被终止并将错误交给router.onError回调
对后台查到的权限链接动态增加路由,并使用一个通配符表示的路由添加在最后,这样当访问的时候访问的路由不存在前面可以访问的路由中,就会跳转到最后一个通配符表示的路由