SpringSecurity权限管理系统实战—四、整合SpringSecurity(上)

目录

SpringSecurity权限管理系统实战—一、项目简介和开发环境准备
SpringSecurity权限管理系统实战—二、日志、接口文档等实现
SpringSecurity权限管理系统实战—三、主要页面及接口实现
SpringSecurity权限管理系统实战—四、整合SpringSecurity(上)
SpringSecurity权限管理系统实战—五、整合SpringSecurity(下)
SpringSecurity权限管理系统实战—六、SpringSecurity整合jwt
SpringSecurity权限管理系统实战—七、处理一些问题
SpringSecurity权限管理系统实战—八、AOP记录用户、异常日志
SpringSecurity权限管理系统实战—九、数据权限的配置

前言

这几天的时间去弄博客了,这个项目就被搁在一边了。
在之前我是用wordpress来搭的博客,用的阿里云的学生机,就卡的不行,体验极差,也没有发布过多少内容。后来又想着自己写一个博客系统,后台部分已经开发了大半,懒癌犯了,就一直搁置了(图片上的所有能点击的接口都实现了)。现在回过去一看,接口十分混乱,冗余。可能不会再用来作为自己的博客了(随便再写写,做个毕设项目吧)

1.png

然后又想着用静态博客,绕来绕去后,最终选用了vuepress来搭建静态博客,部署的时候又顺带着复习了下git的知识(平时idea插件用的搞得我git命令都忘得差不多了)。现在的博客是根据vuepress-theme-roco主题魔改的,给张照片感受下

2.png

已经部署到github pages。可以访问www.codermy.cn查看。 目前还没有备案成功,尚未配置cdn,所以可能会加载有点慢。国内也可以访问 witmy.gitee.io 查看。

一、Spring Security 介绍

Spring Security 是Spring项目之中的一个安全模块,可以非常方便与spring项目集成。自从有了 Spring Boot 之后,Spring Boot 对于 Spring Security 提供了 自动化配置方案,可以零配置使用 Spring Security。

其实Spring Security 最早不叫 Spring Security ,叫 Acegi Security,后来才发展成为Spring的子项目。由于SpringBoot的大火,让Spring系列的技术都得到了非常多的关注度,SpringSecurity同样也沾了一把光。

一般来说,Web 应用的安全性包括两部分:

  1. 用户认证(Authentication)
  2. 用户授权(Authorization)

简单来说,认证就是登录,授权其实就是权限的鉴别,看用户是否具备相应请求的权限。

二、整合SpringSecurity

在SpringBoot中想要使用SpringSecurity,只要添加SpringSecurity的依赖即可

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

这个依赖在最初给的pom中已经有了,不过给注释了,取消掉就可以,其余什么都不用做,启动项目。

启动完成后,我们访问http://localhost:8080或者其中的任何接口,都会重定向到登录页面。

3.png

SpringSecurity默认的用户名是user,密码则在启动项目时会打印在控制台上。

Using generated security password: 21d26148-7f1e-403a-9041-1bc62a034871

21d26148-7f1e-403a-9041-1bc62a034871就是密码,每次启动都会分配不一样的密码。SpringSecurity同样支持自定义密码,只要在application.yml中简单配置一下即可

spring:
    security:
        user:
            name: admin
            password: 123456

输入用户名密码,登录后就能访问index页面了

4.png

三、自定义登录页

SpringSecurity默认的登录页在SpringBoot2.0之后已经做过升级了,以前的更丑,就是一个没有样式的form表单。现在这个虽然好看了不少,但是感觉还是单调了些。

那么我们需要新建一个SpringSecurityConfig类继承WebSecurityConfigurerAdapter

@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

     @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/PearAdmin/**");//放行静态资源
    }
    /**
     * anyRequest          |   匹配所有请求路径
     * access              |   SpringEl表达式结果为true时可以访问
     * anonymous           |   匿名可以访问
     * denyAll             |   用户不能访问
     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
     * permitAll           |   用户可以任意访问
     * rememberMe          |   允许通过remember-me登录的用户访问
     * authenticated       |   用户登录后可访问
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login.html")//登录页面
                .loginProcessingUrl("/login")//登录接口
                .permitAll()
                .and()
                .csrf().disable();//关闭csrf
    }
}

把login.html移动到static目录下,不要忘记把form表单的action替换成/login

<!DOCTYPE html>
<html  xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="utf-8">
        <title></title>
        <link rel="stylesheet" href="/PearAdmin/admin/css/pearForm.css" />
        <link rel="stylesheet" href="/PearAdmin/component/layui/css/layui.css" />
        <link rel="stylesheet" href="/PearAdmin/admin/css/pearButton.css" />
        <link rel="stylesheet" href="/PearAdmin/assets/login.css" />
    </head>
    <body background="PearAdmin/admin/images/background.svg" >
        <form class="layui-form" action="/login" method="post">
            <div class="layui-form-item">
                <img class="logo" src="PearAdmin/admin/images/logo.png" />
                <div class="title">M-S-P Admin</div>
                <div class="desc">
                    Spring Security 权 限 管 理 系 统 实 战
                </div>
            </div>
            <div class="layui-form-item">
                <input id="username" name="username" placeholder="用户名 : " type="text" hover class="layui-input" />
            </div>
            <div class="layui-form-item">
                <input d="password" name="password" placeholder="密 码 : " type="password"  hover class="layui-input" />
            </div>
            <div class="layui-form-item">
                <input type="checkbox" name="" title="记住密码" lay-skin="primary" checked>
            </div>
            <div class="layui-form-item">
                <button style="background-color: #5FB878!important;" class="pear-btn pear-btn-primary login">
                    登 入 
                </button>
            </div>
        </form>
        <script src="/PearAdmin/component/layui/layui.js" charset="utf-8"></script>
        <script>
            layui.use(['form', 'element','jquery'], function() {
                var form = layui.form;
                var element = layui.element;
                var $ = layui.jquery;
                
                $("body").on("click",".login",function(){
                    location.href="index"
                })
            })
        </script>
    </body>
</html>

重启项目查看

5.png

四、动态获取菜单

目前我们的项目还是根据PeaAdmin的menu.json来获取的菜单。这明显不行,没有权限的用户登录后点来点去,发现什么都用不了,这对用户体验来说非常差。所有要根据用户的id来动态的生成菜单。

首先看一下menu.json的格式。

6.png

之后的返回的json格式也要像这样才能被正确解析。

新建一个MenuIndexDto用于封装数据

@Data
public class MenuIndexDto implements Serializable {
    private Integer id;
    private Integer parentId;
    private String title;
    private String icon;
    private Integer type;
    private String href;
    private List<MenuIndexDto> children;
}

MenuDao中新增通过用户id查询菜单的方法

    @Select("SELECT DISTINCT sp.id,sp.parent_id,sp.name,sp.icon,sp.url,sp.type  " +
            "FROM my_role_user sru " +
            "INNER JOIN my_role_menu srp ON srp.role_id = sru.role_id " +
            "LEFT JOIN my_menu sp ON srp.menu_id = sp.id " +
            "WHERE " +
            "sru.user_id = #{userId}")
    @Result(property = "title",column = "name")
    @Result(property = "href",column = "url")
    List<MenuIndexDto> listByUserId(@Param("userId")Integer userId);

MenuService

List<MenuIndexDto> getMenu(Integer userId);

MenuServiceImpl

@Override
    public List<MenuIndexDto> getMenu(Integer userId) {
        List<MenuIndexDto> list = menuDao.listByUserId(userId);
        List<MenuIndexDto> result = TreeUtil.parseMenuTree(list);
        return result;
    }

这里我写了一个工具方法,用于转换返回格式。TreeUtil添加如下方法

public static List<MenuIndexDto> parseMenuTree(List<MenuIndexDto> list){
        List<MenuIndexDto> result = new ArrayList<MenuIndexDto>();
        // 1、获取第一级节点
        for (MenuIndexDto menu : list) {
            if(menu.getParentId() == 0) {
                result.add(menu);
            }
        }
        // 2、递归获取子节点
        for (MenuIndexDto parent : result) {
            parent = recursiveTree(parent, list);
        }
        return result;
    }

    public static MenuIndexDto recursiveTree(MenuIndexDto parent, List<MenuIndexDto> list) {
        List<MenuIndexDto>children = new ArrayList<>();
        for (MenuIndexDto menu : list) {
            if (Objects.equals(parent.getId(), menu.getParentId())) {
                children.add(menu);
            }
            parent.setChildren(children);
        }
        return parent;
    }

MenuController添加如下方法

    @GetMapping(value = "/index")
    @ResponseBody
    @ApiOperation(value = "通过用户id获取菜单")
    public List<MenuIndexDto> getMenu(Integer userId) {
        return menuService.getMenu(userId);
    }

在index.html文件中把菜单数据加载地址 先换成/api/menu/index/?userId=1(这里先写死,之后自定义SpringSecurity的userdetail时再改)

启动项目,查看效果

7.png

这里显示拒绝链接是因为SpringSecurity默认拒绝frame中访问。这里我们可以写一个SuccessHandler设置Header,或者在SpringSecurityConfig重写的configure方法中添加如下配置

http.headers().frameOptions().sameOrigin();

再重启项目,就可以正常访问了。

五、改写菜单路由

之前菜单的路由我们是写再HelloController中的,现在我们规定下格式。新建AdminController

@Controller
@RequestMapping("/api")
@Api(tags = "系统:菜单路由")
public class AdminController {
    @Autowired
    private MenuService menuService;

    @GetMapping(value = "/index")
    @ResponseBody
    @ApiOperation(value = "通过用户id获取菜单")
    public List<MenuIndexDto> getMenu(Integer userId) {
        return menuService.getMenu(userId);
    }

    @GetMapping("/console")
    public String console(){
        return "console/console1";
    }

    @GetMapping("/403")
    public String error403(){
        return "error/403";
    }

    @GetMapping("/404")
    public String error404(){
        return "error/404";
    }

    @GetMapping("/500")
    public String error500(){
        return "error/500";
    }
    @GetMapping("/admin")
    public String admin(){
        return "index";
    }
}

再去相应页面改写下路由就可以

六、图形验证码

验证码主要是防止机器大规模注册,机器暴力破解数据密码等危害。

EasyCaptcha是一个Java图形验证码生成工具,可生成的类型有如下几种

8.png

首先引入maven

<dependencies>
   <dependency>
      <groupId>com.github.whvcse</groupId>
      <artifactId>easy-captcha</artifactId>
      <version>1.6.2</version>
   </dependency>
</dependencies>

新建一个CaptchaController

@Controller
public class CaptchaController {
    
    @RequestMapping("/captcha")
    public void captcha(HttpServletRequest request, HttpServletResponse response) throws Exception {
        CaptchaUtil.out(request, response);
    }
}

再login.html 密码所在的div后面添加如下代码(这里我添加了一下css格式,具体不贴了,自己操作吧)

<div class="layui-form-item">
                <input id="captcha" name="captcha" placeholder="验 证 码:" type="text"  hover class="layui-verify" style="border: 1px solid #dcdfe6;">
                <img src="/captcha" width="130px" height="44px" onclick="this.src=this.src+'?'+Math.random()" title="点击刷新"/>
</div>

重启项目来看一下

9.png

目前只是让验证码在前端绘制了出来,我们如果想要使用,还需要自定义一个过滤器

新建VerifyCodeFilter继承OncePerRequestFilter

@Component
public class VerifyCodeFilter extends OncePerRequestFilter {
    private String defaultFilterProcessUrl = "/login";
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        if ("POST".equalsIgnoreCase(request.getMethod()) && defaultFilterProcessUrl.equals(request.getServletPath())) {
            // 登录请求校验验证码,非登录请求不用校验
            HttpSession session = request.getSession();
            String requestCaptcha = request.getParameter("captcha");
            String genCaptcha = (String) request.getSession().getAttribute("captcha");//验证码的信息存放在seesion种,具体看EasyCaptcha官方解释
            if (StringUtils.isEmpty(requestCaptcha)){
                session.removeAttribute("captcha");//删除缓存里的验证码信息
                throw new AuthenticationServiceException("验证码不能为空!");
            }
            if (!genCaptcha.toLowerCase().equals(requestCaptcha.toLowerCase())) {
                session.removeAttribute("captcha");
                throw new AuthenticationServiceException("验证码错误!");
            }
        }
        chain.doFilter(request, response);
    }
}

最后在SpringSecurity种配置该过滤器

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private VerifyCodeFilter verifyCodeFilter;

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/PearAdmin/**");//放行静态资源
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.headers().frameOptions().sameOrigin();
        http.addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class);
        http.authorizeRequests()
                .antMatchers("/captcha").permitAll()//任何人都能访问这个请求
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login.html")//登录页面 不设限访问
                .loginProcessingUrl("/login")//拦截的请求
                .successForwardUrl("/api/admin")
                .permitAll()
                .and()
            .csrf().disable();//关闭csrf
    }
}

http.addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class);

重启项目,这时需要我们输入正确的验证码后才能进行登录
剩下的一些我们下一节再来完成

在这里插入图片描述

本系列giteegithub中同步更新

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,454评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,553评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,921评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,648评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,770评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,950评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,090评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,817评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,275评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,592评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,724评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,409评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,052评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,815评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,043评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,503评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,627评论 2 350