SprinjgBoot整合shiro和JWT

0. 总体流程

    1. 引入依赖 

    2. 配置工具类
      (ShiroFilfter 自定义需要拦截的请求 ——>
            需要自己编写JWTFilter,用来验证JWT的token )
      (DefaultWebSecurityManager 创建安全管理器,拦截的请求需要用安全管理器来处理)
      (Realm 数据源,安全管理器通过数据源定义的规则对请求进行处理 ——>
            设置凭证匹配器(md5和散列次数,以及开启缓存))
       (ShiroDialect 方言处理类, 解析thymeleaf中的shiro标签)

    3. 创建实体类
      (用户、角色、权限)

    4. 创建自定义realm(继承AuthorizingRealm ,重写认证和授权方法)
      (需要自定义随机盐类,因为系统加载随机盐的类没有序列化所以不能用redis)
    
    5. 自定义缓存管理器和缓存类
      (不能IOC注入,需要用自定义工具类来获取redisTemplate 的 bean对象)
      
请求访问-->是否被拦截-->被拦截:通过JWTFilter来验证token是否合法-->合法:访问页面
                  |                                        |-->不合法(token不存在、token错误、token超时):返回自定义页面
                  |
                  |-->一般是登录页,是否登陆成功-->成功:认证和授权subject并存入redis缓存,生成JWT的token返回给前端,前端要把这个token存入request-header,
                                            

1.引入依赖

<!-- shiro -->
<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-spring</artifactId>
  <version>1.6.0</version>
</dependency>

<!--shiro和thymeleaf整合-->
<dependency>
   <groupId>com.github.theborakompanioni</groupId>
   <artifactId>thymeleaf-extras-shiro</artifactId>
   <version>2.0.0</version>
</dependency>

<!--jwt-->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.4.1</version>
</dependency>

2.创建实体类(展示基础字段)

//用户类
public class User {
    private Integer id;
    private String username;
    private String password;
    private String salt;

    // 角色信息列表
    private List<Role> roleList;
    // 权限列表
    private List<Pers> persList;
}

//角色类
public class Role {
    // id
    private Integer id; 
    // 角色名
    private String name;
}

//权限类
public class Pers {
    // id
    private Integer id;
    // 权限名
    private String name;
    // 权限url
    private String url;
}

3.编写配置类

@Configuration
public class ShiroConfig {

    //创建 ShiroFilfter对象
    // 负责拦截所有的请求
    // 使用安全管理器来认证和授权
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);

        // 添加自定义jwt过滤器,用来验证token
        Map<String, Filter> filterMap = shiroFilterFactoryBean.getFilters();
        filterMap.put("JWTToken",new JWTFilter());
        shiroFilterFactoryBean.setFilters(filterMap);

        //配置系统受限的资源
        //配置系统的公共资源
        Map<String,String> map = new LinkedHashMap<>();
        
        map.put("/user/**","anon");  //开放/user下所有的请求
        map.put("/druid/**","anon");  //开放/druid请求

        //拦截所有请求,放最后
        map.put("/**","JWTToken");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);

        //设置拦截后返回的页面
        shiroFilterFactoryBean.setLoginUrl("/");

        return shiroFilterFactoryBean;
    }

    //创建安全管理器
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm){
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setRealm(realm);

        // 关闭session,没法通过subject获取东西了,只能通过token获取
        /*DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        defaultWebSecurityManager.setSubjectDAO(subjectDAO);*/

        return defaultWebSecurityManager;
    }

    //创建自定义Realm
    @Bean
    public Realm getRealm(){
        // 自己定义的Realm
        CustomRealm customRealm = new CustomRealm();
        // 修改加密的凭证匹配器
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        // 设置md5加密
        credentialsMatcher.setHashAlgorithmName("md5");
        // 设置散列次数
        credentialsMatcher.setHashIterations(1024);
        customRealm.setCredentialsMatcher(credentialsMatcher);

        //开启缓存管理
        customRealm.setCacheManager(new RedisCacheManger());
        customRealm.setCachingEnabled(true);   //开启全局缓存
        customRealm.setAuthenticationCachingEnabled(true);  //开启认证缓存
        customRealm.setAuthenticationCacheName("authCCache");
        customRealm.setAuthorizationCachingEnabled(true);   //开启授权缓存
        customRealm.setAuthorizationCacheName("authZCache");

        return customRealm;
    }

    // thymeleaf 中shiro标签的 方言处理器
    @Bean(name = "shiroDialect")
    public ShiroDialect shiroDialect(){
        return new ShiroDialect();
    }
}

3.1 编写JWTUtil

public class JWTUtil {

    // 自定义私钥
    private static final String TOKEN_SECRET = "^JDF654e2#^%&HDRe";

    //过期时间设置(30分钟)
    private static final long EXPIRE_TIME = 30*60*1000;

    // 自定义加密
    private static final Algorithm ALGORITHM =Algorithm.HMAC256(TOKEN_SECRET);

    // 获取token
    public static String getToken(Map<String,String> map){
        JWTCreator.Builder builder = JWT.create();

        // 过期时间和加密方法
        Date date=new Date(System.currentTimeMillis()+EXPIRE_TIME);

        // 数据填充
        map.forEach((k,v)->{
            builder.withClaim(k,v);
        });

        return builder.withExpiresAt(date).sign(ALGORITHM);
    }

    // 认证,出现错误会报异常
    public static DecodedJWT verifier(String token){
        DecodedJWT verify = JWT.require(ALGORITHM).build().verify(token);
        return verify;
    }
}

3.5 编写JWTFilter

public class JWTFilter extends FormAuthenticationFilter {
    @Override
    public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        HttpServletRequest req = (HttpServletRequest) request;

        String token = req.getHeader("token");

        System.out.println(token);
        
        try{
            JWTUtil.verifier(token);
            return true;
        }catch (Exception e){
            e.printStackTrace();
            saveRequestAndRedirectToLogin(request,response);
            return false;
        }
    }
}

4. 编写自定义盐工具

/**
 * 解决:
 *  shiro 使用缓存时出现:java.io.NotSerializableException: org.apache.shiro.util.SimpleByteSource
 *  序列化后,无法反序列化的问题
 */
public class CustomSaltUtil implements ByteSource, Serializable {
    private static final long serialVersionUID = 1L;

    private  byte[] bytes;
    private String cachedHex;
    private String cachedBase64;

    public CustomSaltUtil(){
    }

    public CustomSaltUtil(byte[] bytes) {
        this.bytes = bytes;
    }

    public CustomSaltUtil(char[] chars) {
        this.bytes = CodecSupport.toBytes(chars);
    }

    public CustomSaltUtil(String string) {
        this.bytes = CodecSupport.toBytes(string);
    }

    public CustomSaltUtil(ByteSource source) {
        this.bytes = source.getBytes();
    }

    public CustomSaltUtil(File file) {
        this.bytes = (new CustomSaltUtil.BytesHelper()).getBytes(file);
    }

    public CustomSaltUtil(InputStream stream) {
        this.bytes = (new CustomSaltUtil.BytesHelper()).getBytes(stream);
    }

    public static boolean isCompatible(Object o) {
        return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream;
    }

    public void setBytes(byte[] bytes) {
        this.bytes = bytes;
    }

    @Override
    public byte[] getBytes() {
        return this.bytes;
    }


    @Override
    public String toHex() {
        if(this.cachedHex == null) {
            this.cachedHex = Hex.encodeToString(this.getBytes());
        }
        return this.cachedHex;
    }

    @Override
    public String toBase64() {
        if(this.cachedBase64 == null) {
            this.cachedBase64 = Base64.encodeToString(this.getBytes());
        }

        return this.cachedBase64;
    }

    @Override
    public boolean isEmpty() {
        return this.bytes == null || this.bytes.length == 0;
    }
    @Override
    public String toString() {
        return this.toBase64();
    }

    @Override
    public int hashCode() {
        return this.bytes != null && this.bytes.length != 0? Arrays.hashCode(this.bytes):0;
    }

    @Override
    public boolean equals(Object o) {
        if(o == this) {
            return true;
        } else if(o instanceof ByteSource) {
            ByteSource bs = (ByteSource)o;
            return Arrays.equals(this.getBytes(), bs.getBytes());
        } else {
            return false;
        }
    }

    private static final class BytesHelper extends CodecSupport {
        private BytesHelper() {
        }

        public byte[] getBytes(File file) {
            return this.toBytes(file);
        }

        public byte[] getBytes(InputStream stream) {
            return this.toBytes(stream);
        }
    }

}

5.编写realm

public class CustomRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;

    /**
     * 用户授权
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //查询角色信息
        List<Role> list = userService.findRolesListByUsername(((String) principalCollection.getPrimaryPrincipal()));
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        //如果不为空
        if(!CollectionUtils.isEmpty(list)){
            // 把角色信息写入subject
            list.forEach(role -> simpleAuthorizationInfo.addRole(role.getName()));
        }

        //查询权限信息
        List<Pers> persList = userService.findPersesListByUsername(((String) principalCollection.getPrimaryPrincipal()));
        if(!CollectionUtils.isEmpty(persList)){
            persList.forEach(pers -> simpleAuthorizationInfo.addStringPermission(pers.getName()));
            return  simpleAuthorizationInfo;
        }

        return null;
    }

    /**
     * 用户认证
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 获取用户名
        String principal = (String) authenticationToken.getPrincipal();
        // 查询数据库中是否存在这个用户
        User user = userService.findUserByUsername(principal);
        // 如果查询到了,就和token中的密码对比
        if(user != null){
            return new SimpleAuthenticationInfo(user.getUsername(),user.getPassword(),
                    new CustomSaltUtil(user.getSalt()),this.getName());
        }
        return null;
    }
}

5.编写control

@RequestMapping("tologin")
    public String login(String username,String password){
        // 获取subject对象
        Subject subject = SecurityUtils.getSubject();

        //通过前端来的密码和用户名生成token
        UsernamePasswordToken token = new UsernamePasswordToken(username,password);

        try {
            //进行验证,这里可以捕获异常,然后返回对应信息
            subject.login(token);
            return "index";
        } catch (UnknownAccountException e) {
            log.error("用户名不存在!", e);
        } catch (AuthenticationException e) {
            log.error("账号或密码错误!", e);
        } catch (AuthorizationException e) {
            log.error("没有权限!", e);
        }
        return "redirect:/";
    }

6 创建缓存

//自定义RedisCache
public class RedisCache<K, V> implements Cache<K, V>{

    private String cacheName;

    private RedisTemplate redisTemplate;

    public RedisCache(){ }

    public RedisCache(String s){
        cacheName = s;
    }

    private RedisTemplate getRedisTemplate(){
        if(redisTemplate == null)
            redisTemplate = ((RedisTemplate) ApplicationContextUtils.getBean("redisTemplate"));
        return redisTemplate;
    }

    @Override
    public V get(K k) throws CacheException {
        if(k == null || k.equals(""))   return null;
        return ((V) getRedisTemplate().opsForHash().get(cacheName, k.toString()));
    }

    @Override
    public V put(K k, V v) throws CacheException {
        getRedisTemplate().opsForHash().put(cacheName,k.toString(),v);
        return null;
    }

    @Override
    public V remove(K k) throws CacheException {
        return ((V) getRedisTemplate().opsForHash().delete(cacheName, k.toString()));
    }

    @Override
    public void clear() throws CacheException {
        getRedisTemplate().delete(this.cacheName);
    }

    @Override
    public int size() {
        return getRedisTemplate().opsForHash().size(this.cacheName).intValue();
    }

    @Override
    public Set<K> keys() {
        return getRedisTemplate().opsForHash().keys(this.cacheName);
    }

    @Override
    public Collection<V> values() {
        return getRedisTemplate().opsForHash().values(this.cacheName);
    }
}

7.创建缓存管理器

public class RedisCacheManger implements CacheManager {
    // 参数 s : 缓存的名字
    @Override
    public <K, V> Cache<K, V> getCache(String s) throws CacheException {
        return new RedisCache<>(s);
    }
}

++.标签

<!--获取subject中的用户信息(用户名)-->
<div shiro:principal=""></div>

<!--认证通过展示的内容-->
<div shiro:authenticated="">
    authenticated
</div>

<!--认证没有通过展示的内容-->
<div shiro:notAuthenticated="">
    notAuthenticated
</div>

<!--授权角色通过展示的内容  admin-->
<div shiro:hasRole="admin">
    admin
</div>

<!--授权资源通过展示的内容 user:delete:*-->
<div shiro:hasPermission="user:delete:*">
    user:delete:*
</div>

--.工具类

@Component
public class ApplicationContextUtils implements ApplicationContextAware {

    private static ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.context = applicationContext;
    }

    //获取bean
    public static Object getBean(String name){
        return context.getBean(name);
    }
}
posted @ 2021-09-16 13:23  一只小白的进修路  阅读(52)  评论(0)    收藏  举报