Springboot2 + Shiro + Redis + Jwt 前后端分离整合(1)

最近看了下shiro这个框架,感觉使用还蛮方便的,话不多说直接上代码。

码云项目地址 : Springboot + shiro + redis + jwt + jpa

新建一个Springboot项目

1.pom.xml加入相关依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.3.2</version>
            <exclusions>
                <exclusion>
                    <groupId>commons-beanutils</groupId>
                    <artifactId>commons-beanutils</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.5.0</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.7.0</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.61</version>
        </dependency>
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>3.2.0</version>
        </dependency>
    </dependencies>

2. application.yml配置

server:
  #端口号
  port: 9002

spring:
  # 数据源设置
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/shirotest?useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&serverTimezone=Asia/Shanghai
    username: root
    password: 123456

  jpa:
    hibernate:
      ddl-auto: update

  redis:
    password:
    timeout: 2000s

3. 实体类及repository

用户表

@Entity
@Data
@Table(name = "t_user")
@FieldDefaults(level = AccessLevel.PRIVATE)
public class UserEntity implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id;
    String userName;
    String password;

    @Transient
    String token;
}

权限表

@Data
@Entity
@Table(name="t_role")
public class RoleEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id;
    String name;
    String roleName;
}

用户权限中间表

@Entity
@Data
@Table(name="t_permission")
public class PermissionEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id;
    Long userId;
    Long roleId;
}

Repository

public interface UserRepository extends JpaRepository<UserEntity,Long> {
    UserEntity findFirstByUserName(String userName);
}

public interface PermissionRepsitory extends JpaRepository<PermissionEntity,Long> {
	List<PermissionEntity> findByUserId(Long userId);
}

4. 配置类

redis配置

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
         /*
             设置这些是 当redis的value设置为Jackson2JsonRedisSerializer,导致shiro反序列化时出错
             直接使用JdkSerializationRedisSerializer不会出错,但是在RedisDesktopManager中,无法查看保存的数据
        */
        ObjectMapper om = new ObjectMapper();
        //在反序列化时忽略在JSON字符串中存在,而在Java中不存在的属性
        om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化方式
        redisTemplate.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }
}

我们使用的是JwtToken来代替sessionId,所以创建了AuthToken类
AuthToken

public class AuthToken implements AuthenticationToken {

    private String token;
    public AuthToken() {
    }
    public AuthToken(String token) {
        this.token = token;
    }
    public String getToken() {
        return token;
    }
    @Override
    public Object getPrincipal() {
        return token;
    }
    @Override
    public Object getCredentials() {
        return token;
    }
}    

AuthRealm 是自定义用户登陆认证、权限设置

  1. 用户认证:初登陆外其余的请求在header头中必须带token这个标志,我们根据token标志到redis判断。我这里做了单点、登出后的token无效、token验证
  2. 授权:在用户认证成功后可以给该用户授予业务权限
@Slf4j
@Component
public class AuthRealm extends AuthorizingRealm {

    @Autowired
    private PermissionRepsitory permissionRepsitory;

    //必须重写,不然会报错
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof AuthToken;
    }

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        log.debug("开始执行授权操作.......");
        System.out.println("调用了授权方法");

        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        //如果身份认证的时候没有传入User对象,这里只能取到userName
        //也就是SimpleAuthenticationInfo构造的时候第一个参数传递需要User对象
        UserEntity user = (UserEntity) principalCollection.getPrimaryPrincipal();
        Long userId = user.getId();

        List<PermissionEntity> list = permissionRepsitory.findByUserId(userId);
        if(!list.isEmpty()){
            list.forEach(o ->{
                authorizationInfo.addStringPermission(o.getRoleId().toString());
            });
        }

        return authorizationInfo;
    }

    //验证用户
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        log.info("验证开始。。。");
        String token = (String) authenticationToken.getCredentials();
        Long userId = TokenUtil.getField(token,"userId",Long.class);

        if(!RedisUtil.hasKey(ShiroConstant.LOGIN_SHIRO_CACHE + userId)){
            throw new AuthenticationException("redis无该用户,登出或被删除,请重新登陆!");
        }

        UserEntity user = (UserEntity) RedisUtil.get(ShiroConstant.LOGIN_SHIRO_CACHE + userId);
        if(!user.getToken().equals(token)){
            throw new AuthenticationException("token不等错误!请重新登陆");
        }

        try{
            TokenUtil.verify(token,user.getUserName(),user.getId());
        }catch (JWTVerificationException e){
            throw new AuthenticationException("token验证出错," + e.getMessage());
        }

        return new SimpleAuthenticationInfo(user, token, this.getName());

    }
}

AuthFilter(自定义拦截器)

@Slf4j
public class AuthFiter extends AuthenticatingFilter{

    /**
     * 生成自定义token
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
        //获取请求token
        String token = getRequestToken((HttpServletRequest) request);
        if (StringUtils.isEmpty(token)) {
            return null;
        }
        return new AuthToken(token);
    }

    /**
     * 步骤1.拦截请求并验证token,成功则进行授权,否则进入onAccessDenied方法
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {

        AuthToken jwtToken = (AuthToken)this.createToken(request,response);
        if(jwtToken != null){
            try {
                String token = jwtToken.getToken();
                // 提交给realm进行登入,如果错误他会抛出异常并被捕获
                getSubject(request, response).login(jwtToken);
                // 如果没有抛出异常则代表登入成功,返回true

                //判断是否要更新token
                String refreshToken = this.refreshToken(token);
                if(!StringUtils.isEmpty(refreshToken)){
                    log.info("更新token时间!!!!!");
                    UserEntity user = (UserEntity) SecurityUtils.getSubject().getPrincipal();
                    user.setToken(refreshToken);
                    //更新redis中用户对象的token
                    RedisUtil.set(ShiroConstant.LOGIN_SHIRO_CACHE + user.getId(),user);
                    //将响应结果加上更新后的token
                    HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
                    httpServletResponse.setHeader("token", refreshToken);
                    httpServletResponse.setHeader("Access-Control-Expose-Headers", "token");
                }
                return true;
            } catch (AuthenticationException e) {
                log.error("登陆失败",e);
                return false;
            }
        }else{
            return false;
        }
    }

    /**
     * 步骤2,若验证成功则进行业务操作,false直接返回。我这边的流程在		        isAccessAllowed已经处理完了,所以这里直接返回false。
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        return false;
    }

    /**
     * 获取请求的token
     */
    private String getRequestToken(HttpServletRequest httpRequest) {
        //从header中获取token
        String token = httpRequest.getHeader("token");
        //如果header中不存在token,则从参数中获取token
        if (StringUtils.isEmpty(token)) {
            token = httpRequest.getParameter("token");
        }
        return token;
    }

    /**
     * 更新token
     */
    private String refreshToken(String token){

        String sign = null;
        DecodedJWT jwt = JWT.decode(token);
        //获取过期时间
        Date exDate = jwt.getExpiresAt();

        //比较过期时间
        boolean refesh = (exDate.getTime() - System.currentTimeMillis()) < TokenUtil.USED_TIME;
        if(refesh){
            //获取token中的数据
            Long userId = TokenUtil.getField(token,"userId",Long.class);
            String userName = TokenUtil.getField(token,"userName",String.class);
            sign = TokenUtil.sign(userName,userId);
        }

       return sign;
    }
}

ShiroConfig(shiro配置)

@Configuration
public class ShiroConfig {

    @Bean(name="shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(org.apache.shiro.mgt.SecurityManager securityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        //自定义过滤器
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("authc",new AuthFiter());
        shiroFilterFactoryBean.setFilters(filterMap);

        LinkedHashMap<String,String> filterChainDefinitionMap = new LinkedHashMap<>();
        //注意过滤器配置顺序 不能颠倒
        // 配置不会被拦截的链接 顺序判断
        filterChainDefinitionMap.put("/user/login", "anon");
        filterChainDefinitionMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        return shiroFilterFactoryBean;
    }

    @Bean(name = "securityManager")
    public org.apache.shiro.mgt.SecurityManager securityManager(AuthRealm realm){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm);
        securityManager.setCacheManager(cacheManager());
        // 关闭Shiro自带的session
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
        return securityManager;
    }

    //使用RedisCacheManager需修改保存的redis的value
    private RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setJedisPool(new JedisPool());
        redisManager.setTimeout(-1);
        redisManager.setPassword("");
        return redisManager;
    }

    @Bean("shiroCacheManager")
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        return redisCacheManager;
    }

    /**
     * 配置Shiro生命周期处理器
     * @return
     */
    @Bean("lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 开启shiro aop注解支持.
     * 使用代理方式;所以需要开启代码支持;
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(org.apache.shiro.mgt.SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * DefaultAdvisorAutoProxyCreator,Spring的一个bean,由Advisor决定对哪些类的方法进行AOP代理。
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
        defaultAAP.setProxyTargetClass(true);
        return defaultAAP;
    }
}

5. 工具类

redisUtil 工具类(自行添加所需要用到的方法)

@Component
public class RedisUtil {

    private static RedisTemplate<String, Object> redisTemplate;

    //工具类是静态方法,用这样的方法将redisTemplate注入
    //@PostConstruct 这个注解也可以,具体使用方法自行网上查询
    @Autowired
    public void setRedisTemplate(RedisTemplate redisTemplate) {
        RedisUtil.redisTemplate = redisTemplate;
    }

    /**
     * 指定缓存失效时间
     * @param key 键
     * @param time 时间(秒)
     * @return
     */
    public static boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据key 获取过期时间
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public static long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    /**
     * 判断key是否存在
     * @param key 键
     * @return true 存在 false不存在
     */
    public static boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除缓存
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public static void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete(CollectionUtils.arrayToList(key));
            }
        }
    }

    /**
     * 普通缓存获取
     * @param key 键
     * @return 值
     */
    public static Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /**
     * 普通缓存放入
     * @param key 键
     * @param value 值
     * @return true成功 false失败
     */
    public static boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
}

TokenUtil工具类

public class TokenUtil {

    //Token过期时间
    private static final long EXPIRE_TIME = 10 * 60 * 1000;
    private static final String TOKEN_SECRET = "shiro123";
    //当前时间与过期时间差小于这个时间,token刷新
    public static final long USED_TIME = 9 * 1000 * 60;

    /**
     * 生成签名
     * 正常Token:Token未过期,且未达到建议更换时间。
	 * 濒死Token:Token未过期,已达到建议更换时间。
	 * 正常过期Token:Token已过期,但存在于缓存中。
	 * 非正常过期Token:Token已过期,不存在于缓存中
     * @param **username**
     * @param **password**
     * @return String
     */
    public static String sign(String username,Long userId) {
        try {
            // 设置过期时间
            // 私钥和加密算法
            // 设置头部信息
            Map<String, Object> header = new HashMap<>(2);
            header.put("Type", "Jwt");
            header.put("alg", "HS256");
            // 返回token字符串
            return JWT.create()
                    .withHeader(header)
                    .withClaim("userName", username)
                    .withClaim("userId",userId)
                    //过期时间
                    .withExpiresAt(new Date(System.currentTimeMillis() + EXPIRE_TIME))
                    .sign(Algorithm.HMAC256(TOKEN_SECRET));
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 检验是否更新token
     */
    public static boolean verify(String token,String username,Long userId)throws JWTVerificationException {
            Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
            JWTVerifier verifier = JWT.require(algorithm)
                                    .withClaim("userName", username)
                                    .withClaim("userId",userId)
                                    .build();
            DecodedJWT jwt = verifier.verify(token);
            return true;
    }

    //获取token中的数据,不需要解密
    public static <T> T getField(String token,String field,Class<T> clazz){
        DecodedJWT jwt = JWT.decode(token);
        return jwt.getClaim(field).as(clazz);
    }
}

ShiroConstant(shiro常量类)

public class ShiroConstant {
    public static final String ROLE_SHIRO_CACHE = "role:userId:";
    public static final String LOGIN_SHIRO_CACHE = "login:userId:";
}

6. controller控制层

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserRepository userRepository;
    @Autowired
    private PermissionRepsitory permissionRepsitory;

    /**
     * 登录
     */
    @PostMapping("/login")
    public Map<String, Object> login(String username, String password)  {
        Map<String, Object> result = new HashMap<>();
        //用户信息
        UserEntity user = userRepository.findFirstByUserName(username);
        //账号不存在、密码错误
        if (user == null) {
            result.put("status", "400");
            result.put("msg", "无该用户");
            return result;
        } else if (!user.getPassword().equals(password)) {
            result.put("status", "400");
            result.put("msg", "账号或密码有误");
            return result;
        } else {
            //生成token,并保存到reids
            String token = TokenUtil.sign(username,user.getId());
            user.setToken(token);
            RedisUtil.set(ShiroConstant.LOGIN_SHIRO_CACHE + user.getId(),user);
            result.put("token",token);
            result.put("status", "200");
            result.put("msg", "登陆成功");

            return result;
        }
    }

    /**
     * 退出
     */
    @PostMapping("/logout")
    public Map<String, Object> logout() {
        Subject sub = SecurityUtils.getSubject();
        log.info("user:" + sub.getPrincipal());
        UserEntity user = (UserEntity)sub.getPrincipal();
        RedisUtil.del(ShiroConstant.LOGIN_SHIRO_CACHE + user.getId(),ShiroConstant.ROLE_SHIRO_CACHE + user.getId());
        Map<String, Object> result = new HashMap<>();
        result.put("status", "200");
        result.put("msg", "登出成功");
        return result;

    }


    //保存用户
    @PostMapping(value = "/save")
    @RequiresPermissions({"1"})
    public Map<String,String> saveUser(UserEntity user){

       userRepository.save(user);
        Map<String,String> result = new HashMap<>();
        result.put("code","200");
        result.put("msg","用户操作成功");
        result.put("obj", JSONObject.toJSONString(user));

        return result;
    }

    //删除用户
    @PostMapping(value = "/del")
    @RequiresPermissions({"2"})
    public Map<String,String> deleteUser(Long userId){

        Map<String,String> result = new HashMap<>();

        Optional<UserEntity> o = userRepository.findById(userId);
        if(o.isPresent()){
            userRepository.deleteById(userId);
            RedisUtil.del(ShiroConstant.ROLE_SHIRO_CACHE +userId,ShiroConstant.LOGIN_SHIRO_CACHE + userId);
            result.put("code","200");
            result.put("msg","用户删除成功");
        }else{
            result.put("code","400");
            result.put("msg","没有这个用户");
        }

        return result;
    }

    //修改用户权限
    @PostMapping(value = "/per")
    @RequiresPermissions({"3"})
    public Map<String,String> permission(PermissionEntity permissionEntity){
        Map<String,String> result = new HashMap<>();

        Optional<UserEntity> o = userRepository.findById(permissionEntity.getUserId());
        if(o.isPresent()){
            RedisUtil.del(ShiroConstant.ROLE_SHIRO_CACHE + permissionEntity.getUserId());
            permissionRepsitory.save(permissionEntity);
            result.put("code","200");
            result.put("msg","权限添加成功");
        }else{
            result.put("code","400");
            result.put("msg","没有这个用户");
        }

        return result;
    }
}

发送请求 ---- 登陆
111登陆成功并在redis保存了user的数据,将登陆后返回的token拿出来,在下一个请求中添加header中添加token
在这里插入图片描述结果报错了!!!报错原因
在这里插入图片描述
解决办法:
在AuthRealm.java重写supports方法

    //必须重写,不然会报错
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof AuthToken;
    }

调用了保存用户
在这里插入图片描述
第一阶段的整合完成啦!!!

  • 6
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值