栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

第七章Spring Security

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

第七章Spring Security

项目进阶,构建安全高效的企业服务

Spring Security

Spring Security底层利用filter(许多专门登录、权限、退出。。。),利用javaee的规范,对整个请求进行拦截。对权限的控制比较靠前,权限不行的话到不了DispatcherServlet,更到不了controller

导包

导过包后会自动对项目进行安全管理,自带登陆页面,控制台有密码,用户名为user

怎么把它的登陆页面换成自己的?用自己数据库里的数据进行登录?

认证授权在业务层进行处理,当前用户的权限怎么体现?可以建立角色表,user的type字段(0普通,1管理员,2版主),用Security做授权时需要一个明确权限的字符串?

1、让user实体类实现UserDetails接口,实现里面的方法。

public class User implements UserDetails {
    
//true:账号未过期
@Override
public boolean isAccountNonExpired() {
    return true;
}

//true:账号未锁定
@Override
public boolean isAccountNonLocked() {
    return true;
}
//true:凭证未过期
@Override
public boolean isCredentialsNonExpired() {
    return true;
}
//true:账号可用
@Override
public boolean isEnabled() {
    return true;
}
//返回用户所具有的权限
    @Override
    public Collection getAuthorities() {
        List list=new ArrayList<>();
        list.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {//每个这个方法封装一个权限(多个封装多个权限)
                switch (type){
                    case 1:
                        return "ADMIN";
                    default:
                        return "USER";
                }
            }
        });
        return list;
    }

2、让userservice实现UserDetailsService接口

根据用户名查用户,自动判断账号密码对不对

public class UserService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        return this.findUserByName(s);
    }
}

3、利用security对项目进行授权

在配置类里面进行配置,继承WebSecurityConfigurerAdapter,重写父类方法

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService userService;


    @Override
    public void configure(WebSecurity web) throws Exception {
        //忽略resource下的所有静态资源
        web.ignoring().antMatchers("/resources
    //1、将指定IP计入UV
    public void recordUV(String ip){
        //先得到key
        String redisKey = RedisKeyUtil.getUVKey(df.format(new Date()));
        redisTemplate.opsForHyperLogLog().add(redisKey,ip);//存进redis
    }

    //统计指定日期范围内的UV
    public long calculateUV(Date start,Date end){
        //先判断日期是否为空
        if(start==null || end==null){
            throw new IllegalArgumentException("参数不能为空!");
        }

        //将该范围内每一天的key合并整理到一个集合里
        List keyList=new ArrayList<>();
        //利用calendar对日期做运算
        Calendar calendar=Calendar.getInstance();//实例化抽象类对象
        calendar.setTime(start);

        //时间<=end才循环
        while (!calendar.getTime().after(end)){
            //得到key
            String key = RedisKeyUtil.getUVKey(df.format(calendar.getTime()));
            //将key加到集合里
            keyList.add(key);
            //calendar加一天
            calendar.add(Calendar.DATE,1);
        }

        //合并这些数据,存放合并后的值
        String redisKey = RedisKeyUtil.getUVKey(df.format(start), df.format(end));//得到合并后的key
        redisTemplate.opsForHyperLogLog().union(redisKey,keyList.toArray());//合并存到redis

        //返回统计的结果
        return redisTemplate.opsForHyperLogLog().size(redisKey);


    }

    

    //统计单日的dau
    public void recordDAU(int userId){
        //得到key
        String rediskey = RedisKeyUtil.getDAUKey(df.format(new Date()));
        //存入redis
        redisTemplate.opsForValue().setBit(rediskey,userId,true);
    }

    //统计某个区间的dau(在该区间内某一天登录了就算是活跃,所以要用or运算)
    public long  calculateDAU(Date start,Date end){
        //先判断日期是否为空
        if(start==null || end==null){
            throw new IllegalArgumentException("参数不能为空!");
        }

        //将该范围内每一天的key合并整理到一个集合里
        //bitmap运算需要数组,所以list集合里面存byte数组
        List keyList=new ArrayList<>();
        //利用calendar对日期做运算
        Calendar calendar=Calendar.getInstance();//实例化抽象类对象
        calendar.setTime(start);

        //时间<=end才循环
        while (!calendar.getTime().after(end)){
            //得到key
            String key = RedisKeyUtil.getDAUKey(df.format(calendar.getTime()));
            //将key加到集合里
            keyList.add(key.getBytes());
            //calendar加一天
            calendar.add(Calendar.DATE,1);
        }

        //得到合并的key
        String redisKey = RedisKeyUtil.getDAUKey(df.format(start), df.format(end));
        //将合并的or运算结果存入redis
         return (long) redisTemplate.execute(new RedisCallback() {
            @Override
            public Object doInRedis(RedisConnection connection) throws DataAccessException {
                connection.bitOp(RedisStringCommands.BitOperation.OR,
                        redisKey.getBytes(),keyList.toArray(new byte[0][0]));
                return connection.bitCount(redisKey.getBytes());
            }
        });

    }

}

表现层分为两步记录和查看,每次请求都要记录,所以要再拦截器里面实现。

@Component
public class DataInterceptor implements HandlerInterceptor {

    @Autowired
    private DataService dataService;

    @Autowired
    private HostHolder hostHolder;

    //在请求之前拦截,只是记录数据,所以要返回true,让其继续向下执行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //统计单日的UV
        //通过request得到ip
        String ip = request.getRemoteHost();
        dataService.recordUV(ip);//调用service统计
        
        //统计单日的DAU
        //得到当前用户,并且判断登陆后才记录
        User user = hostHolder.getUser();
        if (user!=null){
            dataService.recordDAU(user.getId());
        }
        return true;
    }
}

注入拦截器,使其生效

registry.addInterceptor(dataInterceptor)
        .excludePathPatterns("*.css","*.js","*.png","*.jpg","*.jpeg");

控制器

@Controller
public class DataController {

    @Autowired
    private DataService  dataService;

    //打开统计页面的方法,get,post请求都可处理
    @RequestMapping(path = "/data",method ={RequestMethod.GET,RequestMethod.POST} )
    public String getDatePage(){
        return "/site/admin/data";
    }


    //页面上传的是日期的字符串,告诉服务器字符串的格式,他就可以帮你转化,
    // 利用注解@DateTimeFormat
    @RequestMapping(path = "/data/uv",method = RequestMethod.POST)
    public String getUV(@DateTimeFormat(pattern = "yyyy-MM-dd") Date start,
                        @DateTimeFormat(pattern = "yyyy-MM-dd")Date end, Model model){
        long calculateUV = dataService.calculateUV(start, end);
        model.addAttribute("uvResult",calculateUV);//将统计结果存到model
        //将表单的日期也存到model里面,跳转后便于页面显示
        model.addAttribute("uvStartDate",start);
        model.addAttribute("uvEndDate",end);

        return "forward:/data";//转发(这个请求只能完成一部分,下面的部分交给这个请求去完成,即上面那个请求)
    }

    //统计DAU
    @RequestMapping(path = "/data/dau",method = RequestMethod.POST)
    public String getDAU(@DateTimeFormat(pattern = "yyyy-MM-dd") Date start,
                        @DateTimeFormat(pattern = "yyyy-MM-dd")Date end, Model model){
        long dau = dataService.calculateDAU(start, end);
        model.addAttribute("dauResult",dau);//将统计结果存到model
        //将表单的日期也存到model里面,跳转后便于页面显示
        model.addAttribute("dauStartDate",start);
        model.addAttribute("dauEndDate",end);

        return "forward:/data";//转发(这个请求只能完成一部分,下面的部分交给这个请求去完成,即上面那个请求)
    }




}

添加这个功能只能管理员访问

.antMatchers(
        "/discuss/delete",
        "/data/**"
)
.hasAnyAuthority(
        AUTHORITY_ADMIN
)

entity是实体,可以对帖子进行评论也可以对帖子的评论进行评论。(1代表帖子,2代表评论,3代表用户)
entityid,某个类型的具体目标。
targetid,对某个评论进行回复(具有指向性)
status,0表示正常,1表示删除禁用

任务执行和调度

有些任务并不是我们访问服务器才启动的,例如:每隔一个小时计算帖子的分数,每隔半个小时清理服务器上存的文件。

在分布式下为什么使用jdk线程池和Spring线程池会出现问题?需要使用分布式定时任务?

分布式(多个服务器,一个集群),浏览器发给Nginx请求,根据策略Nginx发给服务器,两个服务器代码都一样,对于普通代码没问题,但是对于定时任务,两个同时执行就可能出现问题。

对于QuartZ怎么解决问题呢?

jdk、spring定时任务组件是基于内存的,配置参数在内存里,即服务器1和服务器2没法进行数据共享。QuarZ的定时任务参数保存在数据库里,即便两个服务器同时执行定时任务,会通过数据库加锁的方式抢锁,只有一个线程可以访问,下个线程去访问时,先看一下任务参数是否被修改,修改了,那他就不做了。

使用spring带有的线程池时,首先需要先配置下

#spring普通线程池配置
spring.task.execution.pool.core-size=5
spring.task.execution.pool.max-size=15
spring.task.execution.pool.queue-capacity=100

#spring定时任务的线程池配置
spring.task.scheduling.pool.size=5

另外还需要一个配置类,加上相关注解,配置类、能定时执行、能异步调用

该方法被调用后多长时间被执行,每隔多长时间执行。

只要有程序在跑,他就会被执行。

@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class ThreadPoolTests {

    private static final Logger logger = LoggerFactory.getLogger(ThreadPoolTests.class);

    // JDK普通线程池
    private ExecutorService executorService = Executors.newFixedThreadPool(5);

    // JDK可执行定时任务的线程池
    private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);

    // Spring普通线程池
    @Autowired
    private ThreadPoolTaskExecutor taskExecutor;

    // Spring可执行定时任务的线程池
    @Autowired
    private ThreadPoolTaskScheduler taskScheduler;

    @Autowired
    private AlphaService alphaService;

    private void sleep(long m) {
        try {
            Thread.sleep(m);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 1.JDK普通线程池
    @Test
    public void testExecutorService() {
        Runnable task = new Runnable() {
            @Override
            public void run() {
                logger.debug("Hello ExecutorService");
            }
        };

        for (int i = 0; i < 10; i++) {
            executorService.submit(task);
        }

        sleep(10000);
    }

    // 2.JDK定时任务线程池
    @Test
    public void testScheduledExecutorService() {
        Runnable task = new Runnable() {
            @Override
            public void run() {
                logger.debug("Hello ScheduledExecutorService");
            }
        };

        scheduledExecutorService.scheduleAtFixedRate(task, 10000, 1000, TimeUnit.MILLISECONDS);

        sleep(30000);
    }

    // 3.Spring普通线程池
    @Test
    public void testThreadPoolTaskExecutor() {
        Runnable task = new Runnable() {
            @Override
            public void run() {
                logger.debug("Hello ThreadPoolTaskExecutor");
            }
        };

        for (int i = 0; i < 10; i++) {
            taskExecutor.submit(task);
        }

        sleep(10000);
    }

    // 4.Spring定时任务线程池
    @Test
    public void testThreadPoolTaskScheduler() {
        Runnable task = new Runnable() {
            @Override
            public void run() {
                logger.debug("Hello ThreadPoolTaskScheduler");
            }
        };

        Date startTime = new Date(System.currentTimeMillis() + 10000);
        taskScheduler.scheduleAtFixedRate(task, startTime, 1000);

        sleep(30000);
    }

    // 5.Spring普通线程池(简化)
    @Test
    public void testThreadPoolTaskExecutorSimple() {
        for (int i = 0; i < 10; i++) {
            alphaService.execute1();
        }

        sleep(10000);
    }

    // 6.Spring定时任务线程池(简化)
    @Test
    public void testThreadPoolTaskSchedulerSimple() {
        sleep(30000);
    }

}

使用QuartZ

先需要先初始化表

2、导包


    org.springframework.boot
    spring-boot-starter-quartz

3、

# QuartzProperties 将配置放到数据库里
spring.quartz.job-store-type=jdbc
#调度器的名字
spring.quartz.scheduler-name=communityScheduler
#调度器id自动生成
spring.quartz.properties.org.quartz.scheduler.instanceId=AUTO
spring.quartz.properties.org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
spring.quartz.properties.org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
#是否采用集群
spring.quartz.properties.org.quartz.jobStore.isClustered=true
spring.quartz.properties.org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
spring.quartz.properties.org.quartz.threadPool.threadCount=5

首先定义一个任务(Job接口execute声明我要做什么),具体做什么需要配置(JobDetail名字,组对job进行配置)(Trigger触发器,Job什么时候运行,以什么频率运行),将读取到的配置初始化到数据库,以后直接访问数据库读取配置。

BeanFactory是容器的顶层接口,

FactoryBean可简化Bean的实例化过程:
1、通过FactoryBean封装Bean的实例化过程。
2、将FactoryBean装配到Spring容器里。
3、将FactoryBean注入给其他的Bean。
4、该Bean得到的是FactoryBean所管理的对象实例

例如:

//只有第一次有用,将配置读取到数据库中,以后直接从数据库中读
@Configuration
public class QuartzConfig {

    @Bean  //将JobDetailFactoryBean装配到容器里
    public JobDetailFactoryBean alphaJobDetail(){
        return null;
    }

    @Bean  //我这里的参数需要JobDetail,我将上面bean的方法名传进来,这里得到的是JobDetailFactoryBean管理的对象
    public SimpleTriggerFactoryBean alphaTrigger(JobDetail alphaJobDetail){
        return null;
    }
}

使用用例

1、


public class AlphaJob implements Job {
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println(Thread.currentThread().getName()+":execute a quartz job.");
    }
}

2、


//只有第一次有用,将配置读取到数据库中,以后直接从数据库中读
@Configuration
public class QuartzConfig {

    //配置JobDetail
    @Bean  //将JobDetailFactoryBean装配到容器里
    public JobDetailFactoryBean alphaJobDetail(){
        JobDetailFactoryBean factoryBean=new JobDetailFactoryBean();
        factoryBean.setJobClass(AlphaJob.class);
        factoryBean.setName("alphajob");
        factoryBean.setGroup("alphaJobGroup");
        factoryBean.setDurability(true);//任务不在运行,触发器没有了也不用删,留着
        factoryBean.setRequestsRecovery(true);//任务是不是可恢复的
        return factoryBean;
    }

    //配置触发器
    @Bean  //我这里的参数需要JobDetail,我将上面bean的方法名传进来,这里得到的是JobDetailFactoryBean管理的对象
    public SimpleTriggerFactoryBean alphaTrigger(JobDetail alphaJobDetail){
        SimpleTriggerFactoryBean factoryBean=new SimpleTriggerFactoryBean();
        factoryBean.setJobDetail(alphaJobDetail);//参数名与bean名一致
        factoryBean.setName("alphaTrigger");
        factoryBean.setGroup("alphsTriggerGroup");
        factoryBean.setRepeatInterval(3000);//执行频率
        factoryBean.setJobDataMap(new JobDataMap());//指定那个对象来存状态
        return factoryBean;
    }
}

启动后,配置就会传到数据库里面,每三秒一次

删除任务

@Test
public void testDeleteJob(){
    boolean b = false;
    try {
        b = scheduler.deleteJob(new JobKey("alphajob", "alphaJobGroup"));
        System.out.println(b);
    } catch (SchedulerException e) {
        e.printStackTrace();
    }

}
热帖排行

评论、点赞、加精之后立马计算,效率比较低,启动定时任务去计算,再根据分数排列显示。

思路:

评论、点赞、加精之后不立马计算,将其丢到缓存redis里,定时计算变化的帖子,不变的不算

在新添加的帖子后面也计算分数,并将其存到redis里面

置顶直接放在最顶上,所以不用计算分数。

在加精处也计算将贴子放到redis里

评论:对帖子评论才将其帖子放到redis里

点赞:先判断是对帖子点赞才将贴子放到redis里面

写定时任务(帖子刷新)

1、Job(记录日志)

查帖子,将计算后的帖子同步到es搜索引擎中

声明常量(只需要初始化一次,所以在静态块里面初始化),方便计算

private static final Date epoch;
static {
    try {
        epoch=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2014-08-01 00:00:00");
    } catch (ParseException e) {
        throw new RuntimeException("初始化牛客纪元失败!",e);
    }
}
public class PostScoreRefreshJob  implements Job, CommunityConstant{

    private static final Logger logger= LoggerFactory.getLogger(PostScoreRefreshJob.class);

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private DiscussPostService discussPostService;

    @Autowired
    private LikeService likeService;

    @Autowired
    private ElasticsearchService elasticsearchService;

    private static final Date epoch;
    static {
        try {
            epoch=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2014-08-01 00:00:00");
        } catch (ParseException e) {
            throw new RuntimeException("初始化牛客纪元失败!",e);
        }
    }


    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        //从redis里面取值(先得到key),每一个key都要算一下,反复的操作,所以用BoundSetOperation
        String redisKey = RedisKeyUtil.getPostScoreKey();
        BoundSetOperations operations=redisTemplate.boundSetOps(redisKey);
        //先判断一下缓存中有没有数据,没有变化就不做操作
        if(operations.size()==0){
            logger.info("[任务取消] 没有要刷新的帖子!");
            return;
        }
        //使用日志记录时间中间过程
        logger.info("[任务开始] 正在刷新帖子分数: "+operations.size());
        while (operations.size()>0){//只要redis里面有数据就算
            //集合中弹出一个值
            this.refresh((Integer)operations.pop());
        }
    }

    private void refresh(int postId) {
        //先将贴子查出来
        DiscussPost post = discussPostService.findDiscussDetail(postId);
        //空值判断(帖子被人点赞,但是后来被管理删除)
        if(post==null){
            logger.error("帖子不存在: id= "+ postId);//日志记录错误提示
            return;
        }


        //计算帖子分值(加精-1、评论数、点赞数)
        boolean wonderful = post.getStatus() == 1;
        int commentCount = post.getCommentCount();
        long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, postId);
        //先求权重
        double w=(wonderful? 75 : 0) + commentCount*10 + likeCount * 2;
        //分数=帖子权重+距离天数
        //为了不得到负数,在权重和1之间取最大值。将时间得到的毫秒在换算为天
        double score=Math.log10(Math.max(w,1)+
                (post.getCreateTime().getTime()-epoch.getTime())/(1000 * 3600 * 24));

        //更新帖子的分数
         discussPostService.updateDiscussScore(postId, score);
        //同步搜索对应帖子的数据(先重设帖子的分数,再保存到es)
        post.setScore(score);
        elasticsearchService.saveDiscussPost(post);

    }
}

写好了定时任务还要去配置别忘了

//刷新帖子分数任务
    @Bean  //将JobDetailFactoryBean装配到容器里
    public JobDetailFactoryBean postScoreRefreshJobDetail(){
        JobDetailFactoryBean factoryBean=new JobDetailFactoryBean();
        factoryBean.setJobClass(PostScoreRefreshJob.class);
        factoryBean.setName("postScoreRefreshJob");
        factoryBean.setGroup("communityJobGroup");
        factoryBean.setDurability(true);//任务不在运行,触发器没有了也不用删,留着
        factoryBean.setRequestsRecovery(true);//任务是不是可恢复的
        return factoryBean;
    }

    @Bean
    public SimpleTriggerFactoryBean postScoreRefreshTrigger(JobDetail postScoreRefreshJobDetail){
        SimpleTriggerFactoryBean factoryBean=new SimpleTriggerFactoryBean();
        factoryBean.setJobDetail(postScoreRefreshJobDetail);//参数名与bean名一致
        factoryBean.setName("postScoreRefreshTrigger");
        factoryBean.setGroup("communityJobGroup");
        factoryBean.setRepeatInterval(1000 * 60 *5);//执行频率
        factoryBean.setJobDataMap(new JobDataMap());//指定那个对象来存状态
        return factoryBean;
    }

五分钟更新一次

重构之前查找帖子的mapper

//查找帖子分页显示(userId是动态需要条件,0表示不拼接,其余拼接)
List selectDiscussPosts(int userId,int offset,int limit,int orderMode);