Spring Security实战(+SpringBoot)

我们上一篇文章说了Spring Security的原理。这一篇我们进行实战。项目由Springboot + Spring Security + Mybatis-plus构成。为了让大家更好的理解,我将在最后的部分去解释Security的部门,先介绍项目中的其他模块。篇幅有些长,请耐心看完,相信绘有很大的收获。

一、项目目录:

二、我们先来看pom文件中的依赖:

    <dependencies>
<!--        Spring Security的依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
<!--        SpringBoot的依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
<!--        Mybiatis-plus的依赖-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.1.0</version>
        </dependency>
<!--        连接数据库-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
<!--        lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
            <scope>provided</scope>
        </dependency>

    </dependencies>

如果需要测试方法的话,可以集成对应的test的jar包。

三、application.properties部分

#配置对应端口
server.port=8080
#配置访问路径
server.servlet.context‐path=/security
#程序的名称
spring.application.name = security‐springboot

#视图解析器
spring.mvc.view.prefix=/htmls/
spring.mvc.view.suffix=.html

#数据库连接
spring.datasource.url=jdbc:mysql://localhost:3306/security?useUnicode=true&characterEncoding=utf-8&serverTimezone=CTT
spring.datasource.username=root
spring.datasource.password=
spring.datasource.driver‐class‐name=com.mysql.jdbc.Driver

#mybatis-plus配置
mybatis-plus.type-aliases-package=com.security.hello.security.enity
mybatis-plus.mapper-locations=classpath:mapper/*Mapper.xml

四、数据库和mybatis-plus

表结构:

CREATE TABLE `t_user` (
  `id` bigint(20) NOT NULL COMMENT '用户id',
  `username` varchar(64) NOT NULL,
  `password` varchar(64) NOT NULL,
  `fullname` varchar(255) NOT NULL COMMENT '用户姓名',
  `mobile` varchar(11) DEFAULT NULL COMMENT '手机号',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='用户表';
CREATE TABLE `t_role` (
  `id` varchar(32) NOT NULL,
  `role_name` varchar(255) DEFAULT NULL,
  `description` varchar(255) DEFAULT NULL,
  `create_time` datetime DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  `status` char(1) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `unique_role_name` (`role_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色表';
CREATE TABLE `t_user_role` (
  `user_id` varchar(32) NOT NULL,
  `role_id` varchar(32) NOT NULL,
  `create_time` datetime DEFAULT NULL,
  `creator` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户角色关系表';
CREATE TABLE `t_permission` (
  `id` varchar(32) NOT NULL,
  `code` varchar(32) NOT NULL COMMENT '权限标识符',
  `description` varchar(64) DEFAULT NULL COMMENT '描述',
  `url` varchar(128) DEFAULT NULL COMMENT '请求地址',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='权限表';
CREATE TABLE `t_role_permission` (
  `role_id` varchar(32) NOT NULL,
  `permission_id` varchar(32) NOT NULL,
  PRIMARY KEY (`role_id`,`permission_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色权限关系表';

在一个标准的后台系统是有权限、角色、后台人员几种抽象的实体的,例如一个新闻网站:权限有:发布新闻、审核新闻、删除新闻等,而角色就有新闻发布员(权限:发布新闻)、新闻审核员(新闻审核)、超级管理员(所有权限)等。而权限与角色的对应关系就是多对多。同理呢,角色与后台人员的关系可以是一对一、一对多、多对多都可以。我们这里的t_user就是后台人员表,t_role就是角色表,t_permission就是权限表。还有两个中间表。

首先我们弄清楚我们要用Spring Security去干什么,我们要用这个框架去进行登陆(认证过程),访问资源(授权)。这里的授权就是权限s。

那问题来了我们为啥非得用Spring Security呢?用拦截器不行吗?原因有几点:第一直接写拦截器真的很low,第二框架为我们提供了很方便的api,我们可以写更少的代码,第三更为安全,第四.....。

看不懂数据库sql或者不了解mybatis-plus的小伙伴不必在这过多纠结,只要相信我们对应的api能够查到数据即可。

package com.security.hello.security.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.security.hello.security.enity.UserAdmin;

/**
 * @ClassName UserMapper
 * @Description
 * @Author 
 * @Date 2020/5/4 10:40
 * @Version 1.0
 **/
public interface UserMapper extends BaseMapper<UserAdmin> {

}

这个是查询用户的mapper层,这个因为是Mybatis-plus就是它对Mybatis做了更多的优化。我们没有写方法的原因在于,单表操作不需要写sql,直接写对应的API就可以了。可以在下面看到。

package com.security.hello.security.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.security.hello.security.enity.Permission;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
 * @ClassName PermissionMapper
 * @Description
 * @Author 
 * @Date 2020/5/4 14:22
 * @Version 1.0
 **/
public interface PermissionMapper  extends BaseMapper<Permission> {

    List<String> selectPermissionByUser(@Param("userId") Long id);
}

权限的mapper层,这里为什么有方法呢,因为通过用户的id查询权限,需要连接四张表。建议大家直接写sql。下面对应的xml。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.security.hello.security.mapper.PermissionMapper">
    <select id="selectPermissionByUser" resultType="java.lang.String">
        SELECT
            p.code
        FROM
            t_user_role AS ur
            INNER JOIN t_role AS r ON r.id = ur.role_id
            INNER JOIN t_role_permission AS rp ON rp.role_id = r.id
            INNER JOIN t_permission AS p ON p.id = rp.permission_id
        WHERE
            ur.user_id = #{userId}
    </select>
</mapper>

两个实体类:

package com.security.hello.security.enity;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

/**
 * @ClassName Permission
 * @Description
 * @Author 
 * @Date 2020/5/4 14:23
 * @Version 1.0
 **/
@Data
@TableName("t_permission")
public class Permission {
    private Long id;
    private String code;
    private String description;
    private String url;
}
package com.security.hello.security.enity;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

/**
 * @ClassName User
 * @Description
 * @Author 
 * @Date 2020/5/4 10:35
 * @Version 1.0
 **/
@Data
@TableName("t_user")
public class UserAdmin {
    private Long id;
    private String username;
    private String password;
    private String fullname;
    private String mobile;
}

五、对应的业务层和对外接口

package com.security.hello.security.enity;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

/**
 * @ClassName User
 * @Description
 * @Author 
 * @Date 2020/5/4 10:35
 * @Version 1.0
 **/
@Data
@TableName("t_user")
public class UserAdmin {
    private Long id;
    private String username;
    private String password;
    private String fullname;
    private String mobile;
}
package com.security.hello.security.service.Impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.security.hello.security.enity.UserAdmin;
import com.security.hello.security.mapper.UserMapper;
import com.security.hello.security.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;

/**
 * @ClassName UserServiceImpl
 * @Description
 * @Author 
 * @Date 2020/5/4 10:38
 * @Version 1.0
 **/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, UserAdmin> implements IUserService {

    @Override
    public UserAdmin selectUserByUsername(String username){
        QueryWrapper<UserAdmin> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username",username);
        List<UserAdmin> list = this.baseMapper.selectList(queryWrapper);
        if(list != null && list.size() > 0){
            return list.get(0);
        }else{
            return null;
        }
    }
}

这里的QueryWrapper就是plus为我们提供的简单方法。想必大家都一个问题吧,为啥查询出来是一个列表?不应该是一个用户吗,全局唯一的。这个不是plus不能查出一个数据,而是如果因为系统本身的设计原因,造成用户注册的时候注册了两个用户,这种是存在可能的。查询一个的时候就会报错,而查询列表就会避免这个问题。对于用户来说更加友好。

然后我们来看controller层。

package com.security.hello.security.controller;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @ClassName LoginController
 * @Description 验证用户登陆
 * @Author 
 * @Date 2020/5/3 12:22
 * @Version 1.0
 **/
@RestController
public class LoginController {

    @RequestMapping(value = "/login-success",produces = {"text/plain;charset=UTF-8"})
    public String loginSuccess(){
        return getUserName() + " 登录成功";
    }

    /**
     * 测试资源1
     * @return
     */
    @GetMapping(value = "/r/r1",produces = {"text/plain;charset=UTF-8"})
    public String r1(){
        return " 访问资源1";
    }

    /**
     * 测试资源2
     * @return
     */
    @GetMapping(value = "/r/r2",produces = {"text/plain;charset=UTF-8"})
    public String r2(){
        return " 访问资源2";
    }

    /**
     * 将用户登陆的信息方法了会话里。,认证通过后将身份信息放入SecurityContextHolder上下文,SecurityContext与当前线程进行绑定,
     * 方便获取 用户身份
     */
    private String getUserName(){
        String username = null;
        //当前通过的用户身份
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        //用户身份
        Object principal = authentication.getPrincipal();
        if(principal == null){
            username = "匿名";
        }
        if(principal instanceof org.springframework.security.core.userdetails.UserDetails){
            UserDetails userDetails = (UserDetails)principal;
            username = userDetails.getUsername();
        }else{
            username = principal.toString();
        }
        return username;
    }
}

我们暂时可以先看前三个方法。这个就是我们正常写的一个api。登陆成功之后走的Controller,和两个资源。暂时不用考虑第四个方法。这个我们之后会说。

截止到目前,应该来说是我们学习Security这个框架之前,应该很明确掌握的东西。如果这些东西存在疑问,我觉得应该可以先去学习这些姿势。否则很难理解Security它到底干了啥。

hhhh,忘了还有一个前端页面

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<form action="login" method="post">
    <input type="text" name="username"><br>
    <input type="password" name="password"><br>
    <input type="submit" value="登陆">
</form>
</body>
</html>

六、SpringMVC配置部分

package com.security.hello.security.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @ClassName WebMvcConfig
 * @Description springMvc配置类
 * @Author 
 * @Date 2020/5/3 17:56
 * @Version 1.0
 **/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("redirect:/login-view");
        registry.addViewController("/login-view").setViewName("login");
    }
}

这个配置类可以说帮我们解决了很多的代码编写,是一个非常重要的配置类,我们正好说一下这个WebMvcConfigurer类的常用方法,它经常和Security搭配着使用的。

(1)addInterceptors(InterceptorRegistry registry):这个方法配置的是拦截器,我们在之前的基于session的认证与授权中说过。

(2)addResourceHandlers(ResourceHandlerRegistry registry):自定义资源映射。这个东西也比较常用,业务场景就是自己的服务器作为文件服务器,不利用第三方的图床,就需要一个虚拟路径映射到我们服务器的地址。

 public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/my/**")
    .addResourceLocations("file:E:/my/");
    super.addResourceHandlers(registry);
}

(3)addCorsMappings(CorsRegistry registry):设置跨域问题的,几乎是每个后台服务器都需要配置的东西。

(4)addViewControllers(ViewControllerRegistry registry):我们可以少写Controller达到跳转页面的目的。上面就是访问“/”根路径,就重定向到“/login-view”,然后再访问login的html页面(我们在视图解析器中配过)。

七、今天的主角Security

package com.security.hello.security.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * @ClassName ApplicationConfig
 * @Description SpringSecurity的配置文件
 * @Author 
 * @Date 2020/5/3 12:56
 * @Version 1.0
 **/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    //密码编码器
    @Bean
    public PasswordEncoder passwordEncoder(){
        return NoOpPasswordEncoder.getInstance();
    }

    //安全拦截机制(最重要)
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/r/r1").hasAuthority("1001")
                .antMatchers("/r/r2").hasAuthority("1002")
                .antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
                .anyRequest().permitAll()//除了/r/**,其它的请求可以访问
                .and()
                .formLogin()//允许表单登录
                .loginPage("/login-view")//指定我们自己的登录页
                .loginProcessingUrl("/login")//指定登录处理的URL,也就是用户名、密码表单提交的目的路径
                .successForwardUrl("/login-success")//自定义登录成功的页面地址
                .permitAll()//允许所有用户访问我们的登录页
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                .and()
                .logout()
                .logoutUrl("/logout")//指定退出路径
                .logoutSuccessUrl("/login-view?logout");
    }
}

如果阅读过我上两篇文章的同学,应该就知道了passwordEncoder这个方法是在认证的时候提供校验的规则的,例如加密比较。

另一个configure方法这是我们的安全拦截机制,配置认证与授权的方法。我们下面来详细说明:

第一部分(授权):

(1)csrf().disable():spring security为防止CSRFCross-site request forgery跨站请求伪造)的发生,限制了除了get以外的大多数方 法。这个就是来设置屏蔽CSRF控制。

(2)authorizeRequests():允许基于使用HttpServletRequest限制访问

(3)antMatchers("/r/r1").hasAuthority("1001"):配置这个资源的路径必须有后面的权限标志才能访问否则403。

(4)antMatchers("/r/**").authenticated():所有/r/**的请求必须认证通过才能进行权限。

(5)anyRequest().permitAll():除了/r/**,其它的请求可以访问

第二部分(认证):

(1)formLogin():指定支持基于表单的身份验证。如果未指定FormLoginConfifigurer#loginPage(String),则将生成默认登录页面

(2)loginPage("/login-view"):指定我们自己的登录页,这里是url。

(3)loginProcessingUrl("/login"):前端提供登录接口的url。

(4)successForwardUrl("/login-success"):自定义登录成功的页面地址

(5)permitAll():允许所有用户访问我们的登录页

第三部分(会话):

(1)sessionManagement():允许配置会话管理。

(2)sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED):如果需要就创建一个Session(默认)登录时。

第四部分(退出):

(1)logout():指定退出登陆。

(2)logoutUrl("/logout"):退出的url(api)

(3)logoutSuccessUrl("/login-view?logout"):退出成功访问的页面

如果没有Security,我们的登陆、退出、以及设置自己的session都需要自己在controller和拦截器中编写,现在我们就不需要自己去写了。那有的同学可能问,咋实现的呀?我们先按照顺序具体的说一下。

1、认证和授权:

SpringDataUserDetailService已经重写了UserDetailsService ,我们就可以自定义查询用户信息。

(1)前端的html页面通过访问/login这个api进行登录,提供username和password两个参数。

(2)然后就会通过过滤链进行过滤。在上一章有详解。

(3)从数据库中查询出账号信息和权限

package com.security.hello.security.service.Impl;

import com.security.hello.security.enity.Permission;
import com.security.hello.security.enity.UserAdmin;
import com.security.hello.security.mapper.PermissionMapper;
import com.security.hello.security.service.IUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;

/**
 * @ClassName SpringDataUserDetailService
 * @Description
 * @Author 
 * @Date 2020/5/4 8:59
 * @Version 1.0
 **/
@Slf4j
@Service
public class SpringDataUserDetailService implements UserDetailsService {

    @Autowired
    private IUserService userService;
    @Autowired
    private PermissionMapper permissionMapper;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        //查询用户信息
        UserAdmin userAmin = userService.selectUserByUsername(s);
        log.info("user => {}",userAmin);
        if(userAmin == null){
            return null;
        }
        //查询权限
        List<String> list =  permissionMapper.selectPermissionByUser(userAmin.getId());
        String[] arr = new String[list.size()];
        list.toArray(arr);
        UserDetails user = User.withUsername(userAmin.getUsername()).password(userAmin.getPassword()).authorities(arr).build();
        return user;
    }
}

创建一个UserDetails对象,然后返回。这个时候与Authentication对象中信息进行比较(设置的比较规则)passwordEncoder判断是否认证成功,如果成功下一步,如果失败也可以设置(我这没设置页面)。

(4)跳转登陆成功的页面。

(5)如果接下来访问授权的页面

(6)我们在上面的已经获取了权限放到了UserDetails中,所以可以根据访问的资源url要求进行授权验证。

2、会话

用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保存在会话中。 spring security 提供会话管 理,认证通过后将身份信息放入SecurityContextHolder 上下文, SecurityContext 与当前线程进行绑定,方便获取 用户身份。
@Override 
protected void configure(HttpSecurity http) throws Exception {                 
    http.sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) 
}
机制
描述
always
如果没有 session 存在就创建一个
ifRequired
如果需要就创建一个 Session (默认)登录时
never
SpringSecurity 将不会创建 Session ,但是如果应用中其他地方创建了 Session ,那么 Spring Security将会使用它。
stateless
SpringSecurity 将绝对不会创建 Session ,也不使用 Session
(1)默认情况下, Spring Security 会为每个登录成功的用户会新建一个 Session ,就是 ifRequired
(2)若选用 never ,则指示 Spring Security 对登录成功的用户不创建 Session 了,但若你的应用程序在某地方新建了 session,那么 Spring Security 会用它的。
(3)若使用 stateless ,则说明 Spring Security 对登录成功的用户不会创建 Session 了,你的应用程序也不会允许新建 session。并且它会暗示不使用 cookie ,所以每个请求都需要重新进行身份验证。这种无状态架构适用于 REST API 及其无状态认证机制。
 
还记得我们在controller的第四个方法吗?
private String getUserName(){
        String username = null;
        //当前通过的用户身份
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        //用户身份
        Object principal = authentication.getPrincipal();
        if(principal == null){
            username = "匿名";
        }
        if(principal instanceof org.springframework.security.core.userdetails.UserDetails){
            UserDetails userDetails = (UserDetails)principal;
            username = userDetails.getUsername();
        }else{
            username = principal.toString();
        }
        return username;
    }

设置会话超时:

可以再sevlet容器中设置Session的超时时间,如下设置Session有效期为3600s

spring boot 配置文件:

server.servlet.session.timeout=3600s
session 超时之后,可以通过 Spring Security 设置跳转的路径。
http.sessionManagement() .expiredUrl("/login‐view?error=EXPIRED_SESSION") .invalidSessionUrl("/login‐view?error=INVALID_SESSION");
expired session 过期, invalidSession 指传入的 sessionid 无效。

3、退出

跟登陆一样直接调用api就可以了,退出时发生:

(1)使HTTP Session 无效 (2)清除 SecurityContextHolder (3)跳转到 /login-view?logout。

也可以配置自定义的退出哦。

@Override 
protected void configure(HttpSecurity http) throws Exception { 
    http .authorizeRequests() 
    //... .and() 
    .logout() (1) 
    .logoutUrl("/logout") (2) 
    .logoutSuccessUrl("/login‐view?logout") (3) 
    .logoutSuccessHandler(logoutSuccessHandler) (4) 
    .addLogoutHandler(logoutHandler) (5) 
    .invalidateHttpSession(true); (6) 
}
(1) 提供系统退出支持,使用 WebSecurityConfigurerAdapter 会自动被应用。
(2)设置触发退出操作的 URL ( 默认是 /logout )。
(3)退出之后跳转的 URL 。默认是 /login?logout
(4) 定制的 LogoutSuccessHandler ,用于实现用户退出成功时的处理。如果指定了这个选项那么
logoutSuccessUrl() 的设置会被忽略。
(5) 添加一个 LogoutHandler ,用于实现用户退出时的清理工作 . 默认 SecurityContextLogoutHandler 会被添加 为最后一个 LogoutHandler 
(6)指定是否在退出时让 HttpSession 无效。 默认设置为 true
注意:如果让 logout GET 请求下生效,必须关闭防止 CSRF 攻击 csrf().disable() 。如果开启了 CSRF ,必须使用
post 方式请求 /logout 。
 
 
到此为止,我们的文章到了尾声,但是 Spring Security虽然帮我们优化了很多的东西,但是实际上还是存在着弊端,比如我们登陆的时候传入的参数有验证码这样的参数呢?(可以参考: https://blog.csdn.net/qq_31279347/article/details/88894491
 
这里的代码对于一个完整的系统仍有不合理的地方,这里只是为了让大家初步熟悉Spring Security框架。
 
想要转载请标注原创地址!!!

 

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值