SpringBoot2.x版本与shiro从零到一完整的整合(前后端分离)

前几天使用springoot做了一个项目,我在项目中负责登录验证,以及角色权限管理,角色其实也很简单,老师、学生、管理员等。想写一写自己的心得以及一些配置中遇到的坑,绝对的干货。话不多说,开干.第一次写博客,希望大家看了,给一些中肯的建议,大三也不容易啊。

1.搭建springboot

如果使用IDEA直接创建springboot项目

如果使用eclipse,直接去这个网站下载后导入即可项目搭建路径

pom文件中的相关依赖

	这里只列举几个springboot跟shiro整合的


    
        org.apache.shiro
        shiro-spring
        1.4.0
    
    
    
        org.crazycake
        shiro-redis
        3.1.0
    

这里面可能用到一点redis的知识,不会的自己去学习即可

自定义realm

自定义realm类似于我们的数据源,处理从数据库查询出来的账户密码以及权限验证
需要继承AuthorizingRealm类,并实现里面方法

 //授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    User userNew =(User) principalCollection.getPrimaryPrincipal();
    User user = service.findAllUserAndRoleAndPer(userNew.getUsername());
    List stringRole=new ArrayList<>();
    List stringPermission=new ArrayList<>();
    for(Role role:user.getRoleList()){
        if(role!=null){
            stringRole.add(role.getName());
            for(Permission permission:role.getPermissionList()){
                if(permission!=null){
                    stringPermission.add(permission.getName());
                }
            }
        }
    }
    SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();
    info.addStringPermissions(stringPermission);
    info.addRoles(stringRole);
    return info;
}

//登录验证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String username=(String) authenticationToken.getPrincipal();
    User user = service.findByUsernameBasicInfo(username);
    if(user==null||user.getPassword()==null||"".equals(user.getPassword())){
        return null;
    }
    return new SimpleAuthenticationInfo(user, user.getPassword(), this.getClass().getName());
}

在登录验证中authenticationToken.getPrincipal(),是获取从前台传递过来的用户名,而 User user = service.findByUsernameBasicInfo(username);我自己写的一个方法,根据用户名获取用户信息,如过存在,则将user、password、getName()传递到SimpleAuthenticationInfo里面即可

在授权方法里User userNew =(User) principalCollection.getPrimaryPrincipal();把刚刚登陆成功的那个对象拿出来查询该对象的权限,因为只有你登录成功才能授权,否则登录都失败了,还谈什么授权。根据用户名把roles跟Permissions全都查询出来,存入集合,以待备用。

自定义sessionManager

自定义的sessionManager,用于当你登陆成功,可以获取一串token字符串,每次前台请求过来的时候,带上这段字符串后台验证,判断是否登陆过,是否有该权限或者角色。一般是将token放入请求头header中,需要继承DefaultWebSessionManager

private static final  String AUTHORIZATION="token";

@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {

    String sessionId= WebUtils.toHttp(request).getHeader(AUTHORIZATION);
    if(sessionId!=null){

        request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
                ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE);
        request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId);
        request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
    return sessionId;
    }else{
        return super.getSessionId(request, response);
    }

}

自定义AuthorizationFilter

在配置shiro路径的时候,roles[a,b],通常是某个用户同时有a和b角色才可以,但是实际问题一般是只要含有二者之一即可,所以我们需要重写该类,继承AuthorizationFilter

	 @Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
    Subject subject = getSubject(servletRequest, servletResponse);
    //获取当前访问路径所需要的角色集合
    String[] rolesArray = (String[]) o;
    //没有角色限制,可以直接访问
    if (rolesArray == null || rolesArray.length == 0) {
        //no roles specified, so nothing to check - allow access.
        return true;
    }
    Set roles = CollectionUtils.asSet(rolesArray);

    //当前subject是roles 中的任意一个,则有权限访问
    for(String role : roles){
        if(subject.hasRole(role)){
            return true;
        }
    }
    return false;
}

跨域配置管理(踩坑踩了好几天)

需要继承UserFilter,重写preHandle方法,因为前台需要有token传递过来,这个请求属于复杂请求,每次发送请求前,首先发送options请求过来"探路",所以我们需要先把放行,还有Access-control-Allow-Origin的值不能设置成* 否则后台报错

		 @Override
	    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
	        HttpServletResponse httpResponse = (HttpServletResponse) response;
	        HttpServletRequest httpRequest = (HttpServletRequest) request;
	        if (httpRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
	            setHeader(httpRequest,httpResponse);
	            return true;
	        }else{
	            setHeader(httpRequest,httpResponse);
	            return true;
	        }
	    }
	     private void setHeader(HttpServletRequest request,HttpServletResponse response){
	        //跨域的header设置
	        response.setHeader("Access-control-Allow-Origin", request.getHeader("Origin"));
	        response.setHeader("Access-Control-Allow-Methods", "GET,,POST,PUT,DELETE");
	        response.setHeader("Access-Control-Allow-Credentials", "true");
	        response.setHeader("Access-Control-Allow-Headers", request.getHeader("Access-Control-Request-Headers"));
	        response.setHeader("Content-Type","application/json;charset=UTF-8");

	    }

redis缓存的基本配置

	@Component
	@ConfigurationProperties(prefix = "redis")
	@PropertySource("classpath:application.properties")
	@Data
	public class RedisConfig {
    private Integer port;

    private String host;

    private String password;

    private Integer database;

}

重头戏来了!!!配置最多的类

@Configuration
public class ShiroConfiguration {

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){

        ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        //设置如果未登录时需要跳转的url
        shiroFilterFactoryBean.setLoginUrl("");
        
        //设置登陆成功后跳转的界面或者url,如果是前后端分离,则可以不写该选项
        shiroFilterFactoryBean.setSuccessUrl("");
        
        //登录成功后,但是未授权,则调到未授权的url
        shiroFilterFactoryBean.setUnauthorizedUrl("");
        
        //将自定义的角色过滤器放到map里面
        Map map = new LinkedHashMap<>();
        
        //这里配置好角色过滤器时,拦截路径时的前缀要写成key的值
        map.put("customRoles", 自定义的AuthorizationFilter);
        //自定义的过滤器解决跨域
    	map.put("corsFilter", 自定义的跨域过滤器);
        
        //将自定义的角色过滤器放到shiroFilterFactoryBean里面去
        shiroFilterFactoryBean.setFilters(map);
        
       !!!!注意这里有个坑:一定要使用LinkedHashMap而不是hashmap,因为hashmap是无序的,
   		对于路径的映射,一旦匹配到就会立即返回,我们的配置也是有顺序的,可能有的小伙伴使用了hashmap,路径有时匹配成功,有时失败
        //拦截器路径,同一拦截,注意要使用linkedHashMap保证过滤器的顺序
        Map filterMap=new LinkedHashMap<>();
        
        //key是需要拦截的路径,value采用哪种过滤器,或者那种角色或权限
        //跨域拦截
        filterMap.put("/**", "corsFilter");
        
        //登出过滤器
        filterMap.put("", "logout");
        
        //匿名访问,也就是说无需的登录即可访问
        filterMap.put("", "anon");
        
        //需要登录才能访问的
        filterMap.put("", "authc");
        
        //有相应角色才能访问的,例如管理员才能访问
        filterMap.put("", "customRoles[admin,root]"); //这里对应上面自定义AuthorizationFilter的key
        
        //有相应权限才能访问的,例如有
        filterMap.put("", "perms[*]");
        
        //全局拦截,避免遗漏哪些路径,放到最下面,这里也是要求必须使用linkedhashmap的理由
        filterMap.put("/**", "authc");
        
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
        return shiroFilterFactoryBean;
    }
    //安全管理器
    @Bean
    public SecurityManager securityManager(RedisCacheManager redisCacheManager){
        DefaultWebSecurityManager manager=new DefaultWebSecurityManager();
        //设置会话管理
        manager.setSessionManager(customSessionManager());
        //设置realm
        manager.setRealm(customRealm());
        //设置缓存
        manager.setCacheManager(redisCacheManager);
        return manager;
    }
//自定义realm
    @Bean
    public CustomRealm customRealm(){
        CustomRealm customRealm=new CustomRealm();
        customRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return customRealm;
    }
    //自定义session管理器
    @Bean
    public CustomSessionManager customSessionManager(){
        CustomSessionManager customSessionManager=new CustomSessionManager();
       	customSessionManager.setSessionDAO(redisSessionDAO());
        return customSessionManager;
    }
    //密码匹配器
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher(){
        HashedCredentialsMatcher hashedCredentialsMatcher=new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        hashedCredentialsMatcher.setHashIterations(2);
        return hashedCredentialsMatcher;
    }

    @Autowired
    private   RedisConfig redisConfig;
    //缓存管理
    @Bean
    public RedisManager redisManager(){
        RedisManager redisManager=new RedisManager();
        redisManager.setDatabase(redisConfig.getDatabase());
        redisManager.setHost(redisConfig.getHost());
        redisManager.setPort(redisConfig.getPort());
        redisManager.setPassword(redisConfig.getPassword());
        return redisManager;
    }
    //cache管理器
    @Bean
    public RedisCacheManager redisCacheManager(RedisManager redisManager){
        RedisCacheManager redisCacheManager=new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager);
        //设置过期时间,单位是秒
        redisCacheManager.setExpire(60*30);
        return redisCacheManager;
    }
    //设置sessionDao
    @Bean
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        return redisSessionDAO;
    }

登录的controller

public Map login(String username,String password){
    Map map = new HashMap<>();
    Subject subject= SecurityUtils.getSubject();
    UsernamePasswordToken token=new UsernamePasswordToken(username, password);
    try{
        subject.login(token);
        //获取sessionid,传到前台
        Serializable sessionId = subject.getSession().getId();
        map.put("message", RbacStatus.SUCCESS_IN.getMessage());
        map.put("session_id", sessionId);
        map.put("code", RbacStatus.SUCCESS_IN.getCode());
        return map;
    }catch (Exception e){
        map.clear();
        e.printStackTrace();
        map.put("message", RbacStatus.ERROR_IN.getMessage());
        map.put("code", RbacStatus.ERROR_IN.getCode());
        return map;
    }
}

这里使用redis的原因有两种:
1.在权限校验时,每次需要查询数据库,用户是否拥有该角色或者权限,访问频率较高,而且修改少,正好符合使用redis特点,对于登录来说,访问频率低,不建议使用。
2.可以将前台传递的token放入redis中,类似于分布式session共享,假如项目集群部署时,放入redis,也可以达到共享,唯一的缺点需要自己维护过期时间,可以每次请求后台路径的时候,更新一次时间,或者前台使用js做个定时器也是可以的,emmmm就这样吧。

你可能感兴趣的:(SpringBoot2.x版本与shiro从零到一完整的整合(前后端分离))