SpringSecurity Spring

2022-05-31 约 6324 字 阅读时长13 分钟

SpringSecurity

简介

核心功能

  • 认证(你是谁,用户/设备/系统)
  • 验证(你能干什么,也叫权限控制/授权,允许执行的操作)

原理

基于Filter、Servlet、AOP 实现身份认证和权限验证

起步

demo

  1. 导入spring-boot-starter-security 起步依赖

    groovy
    1implementation 'org.springframework.boot:spring-boot-starter-security'
  2. 无任何配置下启动程序,console会输出随机生成的密码

    image-20220531213917852

  3. 创建controller,然后浏览器访问,此时地址会被拦截,需要输入正确的用户名和密码(临时生成的)才能进入controller

    tex
    1默认用户名:user
    2随机密码:6899342d-50a7-4095-bfea-7efecc6d980a
  4. 自定义用户名和密码,修改配置文件;此时console不再打印随机生成密码

    properties
    1spring.security.user.name=lei
    2spring.security.user.password=123456
  5. 不使用安全认证机制(排除),启动类的注解上排除

    java
    1@SpringBootApplication(exclude = {SecurityAutoConfiguration.class})

自定义认证(内存)

通过继承 WebSecurityConfigurerAdapter 类,并重写其中某些方法实现自定义认证功能

@EnableWebSecurity注解开启安全机制

@EnableGlobalMethodSecurity(prePostEnabled = true) 可以开启方法级别的权限认证(通过角色),同一个用户可以有多个角色

  1. 密码编码器,springsecurity实现了许多密码编码器,这里采用 BCryptPasswordEncoder实现类,设置内存用户时对密码进行加密

    java
    1/**
    2 * 密码编码器
    3 */
    4@Bean
    5public PasswordEncoder passwordEncoder(){
    6    return new BCryptPasswordEncoder();
    7}
  2. 配置类

    java
     1@Configuration
     2@EnableWebSecurity
     3@EnableGlobalMethodSecurity(prePostEnabled = true)
     4public class WebAutConfig extends WebSecurityConfigurerAdapter {
     5
     6    @Autowired
     7    private PasswordEncoder passwordEncoder;
     8
     9    /**
    10     * 重写方法,添加内存用户
    11     */
    12    @Override
    13    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    14        // 添加两个内存用户,并指定角色;同一个用户可以有多个角色
    15        auth.inMemoryAuthentication().withUser("lei")
    16                .password(passwordEncoder.encode("1234")).roles("normal");
    17        auth.inMemoryAuthentication().withUser("yu")
    18                .password(passwordEncoder.encode("1234")).roles("admin","normal");
    19    }
    20
    21}
  3. controller,配置方法允许哪些角色访问

    java
     1@RestController
     2@RequestMapping("hello")
     3public class HelloController {
     4    @RequestMapping("sayCommon")
     5    //该方法 normal 和 admin 角色都可以访问
     6    @PreAuthorize("hasAnyRole('normal 和','admin')")
     7    public Object sayCommon(){
     8        return "Hello sayCommon";
     9    }
    10    @RequestMapping("sayAdmin")
    11    //该方法只允许 admin 角色访问
    12    @PreAuthorize("hasAnyRole('admin')")
    13    public Object sayAdmin(){
    14        return "Hello sayAdmin";
    15    }
    16}

基于数据库用户认证

从数据库中获取用户信息(用户名称、密码、角色)

在spring secrity中对用户信息表示类为UserDetails,UserDetails是一个接口,高度抽象用户信息

集成postgres数据库

  1. 导入依赖

    groovy
    1implementation 'org.postgresql:postgresql'
    2implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
  2. 配置默认数据源

    properties
    1spring.datasource.driver-class-name=org.postgresql.Driver
    2spring.datasource.url=jdbc:postgresql://myrapi.io:5432/mydb?useSSL=false&connectTimeout=10000&socketTimeout=30000&serverTimezone=GMT%2B8&zeroDateTimeBehavior=ROUND&rewriteBatchedStatements=true&characterEncoding=utf8&allowMultiQueries=true
    3spring.datasource.username=postgres
    4spring.datasource.password=123456
    5spring.datasource.password=123456
    6## 开启根据实体类自动建表、显示sql、数据库类型
    7spring.jpa.generate-ddl=true
    8spring.jpa.show-sql=true
    9spring.jpa.database=mysql
  3. 创建实体类和dao接口

    java
     1//实体类
     2@Entity(name = "USER_INFO")
     3@Data
     4public class UserInfo implements Serializable {
     5    private static final long serialVersionUID = -329565948685722547L;
     6    @Id
     7    // 通过序列化生成主键id,会在数据库中维护一张序列化表(tb_seq)
     8    @GeneratedValue(generator = "tb_seq",strategy = GenerationType.TABLE)
     9    private Long userId;
    10
    11    @Column(name = "username",length = 20)
    12    private String username;
    13
    14    @Column(name = "password",length = 80)
    15    private String password;
    16
    17    @Column(name = "role",length = 10)
    18    private String role;
    19
    20}
    21
    22//dao接口
    23public interface UserInfoDao extends JpaRepository<UserInfo,Long> {
    24    UserInfo findByUsername(String username);
    25}
  4. 通过@PostConstruct生成初始化数据,使用一次

    java
     1@Autowired
     2private UserInfoDao userInfoDao;
     3
     4@PostConstruct		//该bean注入IOC后,自动调用该方法
     5public void init(){
     6    List<UserInfo> userInfos=new ArrayList<>();
     7    UserInfo u1=new UserInfo();
     8    u1.setUsername("lei");
     9    u1.setPassword(passwordEncoder.encode("1234"));
    10    u1.setRole("admin");
    11
    12    UserInfo u2=new UserInfo();
    13    u2.setUsername("yu");
    14    u2.setPassword(passwordEncoder.encode("1234"));
    15    u2.setRole("normal");
    16
    17    userInfos.add(u1);
    18    userInfos.add(u2);
    19
    20    userInfoDao.saveAll(userInfos);
    21}

实现UserDetailsService

java
 1//这里需要指定bean的名字,不然会因为有多个UserDetailsService类型bean不能注入
 2@Service("chUserDetailsService")  
 3public class UserDetailsServiceImpl implements UserDetailsService {
 4    @Autowired
 5    private UserInfoDao userInfoDao;
 6
 7    @Override
 8    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
 9        if (ObjectUtil.isNotEmpty(username)){
10            UserInfo byUsername = userInfoDao.findByUsername(username);
11            if (byUsername!=null){
12                //构建User对象,该对象为UserDetails接口的实现
13                return new User(byUsername.getUsername(),byUsername.getPassword(),
14                        List.of(new SimpleGrantedAuthority("ROLE_"+byUsername.getRole())));
15            }
16        }
17        return null;
18    }
19}

使用UserDetailsService认证

java
 1@Configuration
 2@EnableWebSecurity
 3@EnableGlobalMethodSecurity(prePostEnabled = true)
 4public class WebAutConfig extends WebSecurityConfigurerAdapter {
 5
 6    @Autowired
 7    private PasswordEncoder passwordEncoder;
 8
 9    @Resource
10    private UserInfoDao userInfoDao;
11
12    @Resource
13    private UserDetailsService chUserDetailsService;
14
15    /**
16     * 重写方法,通过自定义的实现类进行认证
17     */
18    @Override
19    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
20        auth.userDetailsService(chUserDetailsService).passwordEncoder(passwordEncoder);
21    }
22
23}

基于角色权限

介绍

认证和授权

  • Authentication:认证,访问者是谁,是否是系统有效用户
  • Authorization:授权,用户能访问系统中的哪些资源及能做哪些操作

RBAC模型是什么?

RBAC(Role-Based Access Control)即:基于角色的权限控制。通过角色关联用户,角色关联权限的方式间接赋予用户权限

img

用户拥有角色 –> 角色控制权限

这样在新建用户时只需要分配角色就可以实现权限控制

通常来说rbac模型中表结构

  1. 用户表:用户认证(用户名、密码、是否启用等相关信息)
  2. 角色表:定义角色信息(角色名称、角色描述)
  3. 用户和角色关系表:用户和角色多对多关系
  4. 权限表:角色可以拥有哪些权限

认证相关类

UserDetails接口

java
 1//权限集合
 2Collection<? extends GrantedAuthority> getAuthorities();
 3
 4//账户是否过期
 5boolean isAccountNonExpired();
 6
 7//账户是否锁定
 8boolean isAccountNonLocked();
 9
10//用户的凭据(密码)是否已过期。过期的凭据会阻止身份验证
11boolean isCredentialsNonExpired();
12
13//账户是否启用
14boolean isEnabled();

UserDetails接口默认实现类为 org.springframework.security.core.userdetails.User;也可自己自定义 UserDetails 接口实现,作为系统中的用户类

UserDetailsService

主要作用:获取用户信息得到一个UserDetails对象

java
1//根据用户名称获取一个 UserDetails 对象
2UserDetails loadUserByUsername(String username);

UserDetailsService 默认提供的实现类;其中有基于内存的、基于数据库的、基于缓存的 用户信息管理

image-20220604172844523

基于数据库的访问认证,在 JdbcDaoImpl 该类的目录下会提供ddl语句

基于JDBC的实现类使用实例

  • 找到JdbcDaoImpl 该类的目录下会提供ddl语句,创建表结构
  • 声明bean,并创建两个内存用户
java
 1@Resource
 2private DataSource dataSource;
 3@Autowired
 4private PasswordEncoder passwordEncoder;
 5@Bean
 6public UserDetailsService jdbcUserDetailsService(){
 7    JdbcUserDetailsManager manager = new JdbcUserDetailsManager();
 8    manager.setDataSource(dataSource);		//这里注意设置数据源
 9    
10    //以下创建用户的代码,当数据库已经存在同名用户会报错
11    //及 以下创建用户的代码只可运行一次
12    manager.createUser(User.withUsername("zs")
13                       .password(passwordEncoder.encode("123")).roles("ADMIN").build());
14    manager.createUser(User.withUsername("ls")
15                       .password(passwordEncoder.encode("123")).roles("ADMIN","NORMAL").build());
16    return manager;
17}
  • 在 继承了WebSecurityConfigurerAdapter类的子类中配置UserDetailsService

    java
     1@Resource
     2private UserDetailsService jdbcUserDetailsService;
     3@Resource
     4private PasswordEncoder passwordEncoder;
     5
     6//通过重写两个方法任意一个均可设置 UserDetailsService
     7//1.方法一
     8@Override
     9protected void configure(HttpSecurity http) throws Exception {
    10    super.configure(http);
    11    http.userDetailsService(jdbcUserDetailsService);
    12}
    13
    14//2.方法二
    15@Override
    16protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    17    auth.userDetailsService(jdbcUserDetailsService).passwordEncoder(passwordEncoder);
    18}

自定义认证

创建库表

image-20220606232738133

sql
 1CREATE TABLE "sys_role" (
 2  "id" varchar(32) NOT NULL,
 3  "role_name" varchar(20) NOT NULL,
 4  "role_memo" varchar(255),
 5  PRIMARY KEY ("id")
 6);
 7
 8CREATE TABLE "sys_user" (
 9  "id" varchar(32) NOT NULL,
10  "username" varchar(20) NOT NULL,
11  "password" varchar(100) NOT NULL,
12  "realname" varchar(60) NOT NULL,
13  "isexpired" varchar(10) NOT NULL,
14  "isenable" varchar(10) NOT NULL,
15  "islocked" varchar(10) NOT NULL,
16  "create_time" date NOT NULL,
17  "update_time" date,
18  "last_login_time" date,
19  PRIMARY KEY ("id"),
20  CONSTRAINT "username_unique" UNIQUE ("username")
21);
22COMMENT ON COLUMN "sys_user"."isexpired" IS '是否过期';
23COMMENT ON COLUMN "sys_user"."isenable" IS '是否启用';
24COMMENT ON COLUMN "sys_user"."islocked" IS '是否锁定';
25COMMENT ON CONSTRAINT "username_unique" ON "sys_user" IS '用户名唯一';
26
27CREATE TABLE "sys_user_role" (
28  "id" varchar(32) NOT NULL,
29  "user_id" varchar(32) NOT NULL,
30  "role_id" varchar(32) NOT NULL,
31  PRIMARY KEY ("id")
32);

依赖与配置

  1. 依赖

    groovy
     1dependencies {
     2    implementation 'org.springframework.boot:spring-boot-starter-web'
     3    implementation 'org.springframework.boot:spring-boot-starter-security'
     4    implementation 'org.postgresql:postgresql'
     5    implementation 'com.baomidou:mybatis-plus-boot-starter:3.5.2'
     6    compileOnly 'org.projectlombok:lombok'
     7    annotationProcessor 'org.projectlombok:lombok'
     8    implementation 'cn.hutool:hutool-all:5.8.2'
     9    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    10}
  2. application配置

    properties
    1spring.datasource.driver-class-name=org.postgresql.Driver
    2spring.datasource.url=jdbc:postgresql://myrapi.io:5432/mydb?useSSL=false&connectTimeout=10000&socketTimeout=30000&serverTimezone=GMT%2B8&zeroDateTimeBehavior=ROUND&rewriteBatchedStatements=true&characterEncoding=utf8&allowMultiQueries=true
    3spring.datasource.username=postgres
    4spring.datasource.password=123456
    5
    6mybatis-plus.mapper-locations=classpath*:/mapper/**/*.xml
  3. 启动类配置

    java
     1@SpringBootApplication
     2@MapperScan(basePackages = "com.lei.ch01security.**.dao")
     3public class SecurityApplication {
     4    public static void main(String[] args) {
     5        SpringApplication.run(SecurityApplication.class, args);
     6    }
     7    /**
     8     * 密码编码器
     9     */
    10    @Bean
    11    public PasswordEncoder passwordEncoder(){
    12        return new BCryptPasswordEncoder();
    13    }
    14}

相关类创建

  1. 示例 重点 SysUser 实体类 创建;需要实现UserDetails接口

    java
     1@Data
     2@TableName("sys_user")
     3public class SysUser implements Serializable, UserDetails {
     4    private static final long serialVersionUID = 771578657753428713L;
     5
     6    @TableId(type = IdType.INPUT)
     7    private String id;
     8
     9    @TableField("username")
    10    private String username;
    11
    12    @TableField("password")
    13    private String password;
    14
    15    @TableField("realname")
    16    private String realname;
    17
    18    @TableField("isenable")
    19    private String isenable;    //是否启用
    20
    21    @TableField("islocked")
    22    private String islocked;    //是否锁定
    23
    24    @TableField("isexpired")
    25    private String isexpired;       //是否过期
    26
    27    @TableField("create_time")
    28    private Date createTime;
    29
    30    @TableField("update_time")
    31    private Date updateTime;
    32
    33    @TableField("last_login_time")
    34    private Date lastLoginTime;
    35
    36    @TableField(exist = false)
    37    @Getter(AccessLevel.NONE)
    38    @Setter(AccessLevel.NONE)
    39    private List<GrantedAuthority> grantedAuthorityList;
    40
    41    @Override
    42    public Collection<? extends GrantedAuthority> getAuthorities() {
    43        if (ObjectUtil.isEmpty(grantedAuthorityList) && ObjectUtil.isNotEmpty(username)) {
    44            grantedAuthorityList=new ArrayList<>();
    45            //查询关联角色列表
    46            SysUserRoleDao userRoleDao = SpringUtil.getApplicationContext().getBean(SysUserRoleDao.class);
    47            List<String> roleList = userRoleDao.selectRolesByUserId(id);
    48            for (String s : roleList) {
    49                //角色名称以"ROLE_"打头
    50                grantedAuthorityList.add(new SimpleGrantedAuthority(s));
    51            }
    52        }
    53        return grantedAuthorityList;
    54    }
    55
    56    /**
    57     * 账户非过期
    58     */
    59    @Override
    60    public boolean isAccountNonExpired() {
    61        return Objects.equals(isexpired, Constants.ACCOUNT_N);
    62    }
    63
    64    /**
    65     * 账户非锁定
    66     */
    67    @Override
    68    public boolean isAccountNonLocked() {
    69        return Objects.equals(islocked, Constants.ACCOUNT_N);
    70    }
    71
    72    /**
    73     * 凭证
    74     */
    75    @Override
    76    public boolean isCredentialsNonExpired() {
    77        return isAccountNonExpired() && isAccountNonLocked() && isEnabled();
    78    }
    79
    80    /**
    81     * 启用
    82     */
    83    @Override
    84    public boolean isEnabled() {
    85        return Objects.equals(isenable, Constants.ACCOUNT_Y);
    86    }
    87}
  2. 其他相关接口

    java
     1//service接口
     2public interface SysUserService extends IService<SysUser> {
     3}
     4
     5//service实现类
     6@Service
     7public class SysUserServiceImpl extends ServiceImpl<SysUserDao, SysUser> implements SysUserService {
     8}
     9
    10//dao接口
    11public interface SysUserDao extends BaseMapper<SysUser> {
    12}
    13
    14//mapper文件
    15/*
    16    <?xml version="1.0" encoding="UTF-8" ?>
    17    <!DOCTYPE mapper
    18            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    19            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    20    <mapper namespace="com.lei.ch01security.dao.SysUserDao">
    21
    22    </mapper>
    23*/
    24
    25//.......其余根据表结构创建类似这些类    

初始化数据

创建相关表结构的mapper,dao,service,serviceimpl;然后初始化数据

初始化数据

java
 1@Resource
 2private SysUserService userService;
 3@Resource
 4private SysRoleService roleService ;
 5@Resource
 6private SysUserRoleService userRoleService;
 7
 8@PostConstruct
 9public void jdbcInit(){
10    //在这里可以进行初始化数据,@PostConstruct标注的方法,在该bea注入IOC时会执行
11    //通过userService roleService userRoleService进行数据的插入,注意密码需要配置加密
12}

配置框架

配置安全框架从数据库加载用户信息

  1. 实现UserDetailsService接口,加载库表用户

    java
     1@Service("chUserDetailsService")
     2public class UserDetailsServiceImpl implements UserDetailsService {
     3    @Resource
     4    private SysUserService userService;
     5    @Override
     6    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
     7        SysUser byUsername = userService.getUserByUsername(username);
     8        byUsername.getAuthorities();	//执行一次该方法,原因可看上面 SysUser 实体类
     9        return byUsername;
    10    }
    11}
  2. 配置安全框架 (WebSecurityConfigurerAdapter 的重写类)

    java
     1@Configuration
     2@EnableWebSecurity
     3@EnableGlobalMethodSecurity(prePostEnabled = true)
     4public class WebAutConfig extends WebSecurityConfigurerAdapter {
     5
     6    @Autowired
     7    private PasswordEncoder passwordEncoder;
     8
     9    @Resource
    10    private UserDetailsService chUserDetailsService;
    11
    12    /**
    13     * 重写方法,添加内存用户
    14     */
    15    @Override
    16    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    17        auth.userDetailsService(chUserDetailsService).passwordEncoder(passwordEncoder);
    18    }
    19
    20    @Override
    21    protected void configure(HttpSecurity http) throws Exception {
    22        http.authorizeRequests()    //允许RequestMatcher进行权限控制
    23                .antMatchers("/index").permitAll()      //该地址放行
    24            	// 注 : 这个地方角色名会默认添加前缀 "ROLE_"
    25                .antMatchers("/access/admin/**").hasRole("ADMIN")   //该地址只允许ADMIN角色访问
    26                .antMatchers("/access/normal/**").hasAnyRole("NORMAL","ADMIN")     //该地址允许NORMAL和ADMIN角色访问
    27                .anyRequest().authenticated()   //其他地址任何角色验证都可访问
    28                .and()
    29                .formLogin();
    30    }
    31}

其他配置

自定义登录页

security框架内置了许多过滤器,验证用户是否登录也是通过过滤器实现

找到 UsernamePasswordAuthenticationFilter 该过滤器,可以看到 默认的登陆方法以及字段名

默认登陆方法为 /login ,表单值 username password ,采用POST方式提交数据

java
1public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
2
3	public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
4
5	public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
6
7	private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login",
8			"POST");
9    .........
  1. 创建登录页 在 resources/static 目录下创建 登录页 login.html

    html
    1<form method="post" action="/login">
    2    用户名:<input type="text" name="username">
    3    密码:<input type="password" name="password">
    4    <input type="submit" placeholder="登录">
    5</form>
  2. 配置框架;登陆成功配置、登陆失败配置、登录用户名密码的字段名,对登陆界面放行

    java
     1@Override
     2protected void configure(HttpSecurity http) throws Exception {
     3    http.authorizeRequests()    //允许RequestMatcher进行权限控制
     4        .antMatchers("/index","/login.html","/index.html").permitAll()      //该地址放行
     5        .antMatchers("/access/admin/**").hasRole("ADMIN")   //该地址只允许ADMIN角色访问
     6        .antMatchers("/access/normal/**").hasAnyRole("NORMAL","ADMIN")     //该地址允许NORMAL和ADMIN角色访问
     7        .anyRequest().authenticated()   //其他地址任何角色验证都可访问
     8        .and()
     9        .formLogin()    //表单登录
    10        .usernameParameter("username")     //配置表单登录的用户名字段
    11        .passwordParameter("password")     //配置表单登录的密码字段
    12        .loginPage("/login.html")       //登录页
    13        .loginProcessingUrl("/login")       //登录方法
    14        .failureForwardUrl("/err.html")		//登陆失败跳转的url,可以跳转静态页面 也可以跳转controller 地址进行处理
    15        .successForwardUrl("/index.html")	//登陆成功跳转的url,可以跳转静态页面 也可以跳转controller 地址进行处理
    16        .and()
    17        .csrf().disable();      //禁用 csrf
    18}

AJAX登陆配置

  1. 创建登录页,引入jquery.js,并编写登录逻辑;注意:jquery.js如果是放在本地的,后台需配置非登录用户也可以访问

    html
     1<head>
     2    <meta charset="UTF-8">
     3    <title>登录页</title>
     4    <script src="js/jquery-3.6.0.min.js"></script>
     5</head>
     6<body>
     7<h1>登录页</h1>
     8    用户名:<input type="text" name="username">
     9    密码:<input id="password" type="password" name="password">
    10<button id="loginBtn"> 登录 </button>
    11</body>
    12<script>
    13    $(document).ready(function(){
    14        $("#loginBtn").on("click",function (){
    15            $.ajax({
    16                url:'/login',
    17                method:'post',
    18                data:{
    19                    username:$('input[name="username"]').val(),
    20                    password:$('input[name="password"]').val()
    21                },
    22                success:(res)=>{
    23                    if (res.code !== 0){
    24                        alert("登陆失败:"+res.msg)
    25                    }else{
    26                        alert("登陆成功!!")
    27                    }
    28                },
    29            })
    30        })
    31    });
    32</script>
  2. 配置框架;登陆成功配置、登陆失败配置、登录用户名密码的字段名,对登陆界面放行,以及静态资源的放行

    java
     1@Override
     2protected void configure(HttpSecurity http) throws Exception {
     3    http.authorizeRequests()    //允许RequestMatcher进行权限控制
     4        .antMatchers("/index","/login.html","/index.html","/**/**.js").permitAll()      //该地址放行
     5        .antMatchers("/access/admin/**").hasRole("ADMIN")   //该地址只允许ADMIN角色访问
     6        .antMatchers("/access/normal/**").hasAnyRole("NORMAL","ADMIN")     //该地址允许NORMAL和ADMIN角色访问
     7        .anyRequest().authenticated()   //其他地址任何角色验证都可访问
     8        .and()
     9        .formLogin()    //表单登录
    10        .usernameParameter("username")     //配置表单登录的用户名字段
    11        .passwordParameter("password")     //配置表单登录的密码字段
    12        .loginPage("/login.html")       //登录页
    13        .loginProcessingUrl("/login")       //登录方法
    14        .failureHandler((request, response, err) -> {        //登陆失败处理
    15            response.setContentType("application/json;charset=utf-8");
    16            PrintWriter out = response.getWriter();
    17            //使用jackson写入json对象
    18            ObjectMapper om=new ObjectMapper();
    19            om.writeValue(out, Map.of("code",-1,"msg", err.getMessage()));
    20            out.flush();
    21            out.close();
    22        })
    23        .successHandler((request, response, authentication) -> {    //登陆成功处理
    24            response.setContentType("application/json;charset=utf-8");
    25            PrintWriter out = response.getWriter();
    26            //使用jackson写入json对象
    27            ObjectMapper om=new ObjectMapper();
    28            om.writeValue(out, Map.of("code",0,"msg", "登陆成功"));
    29            out.flush();
    30            out.close();
    31        })
    32        .and()
    33        .csrf().disable();      //禁用 csrf
    34}

无论是登录成功或者登陆失败都会有两个相关处理方法,url 和 handler,选择一个就可

successForwardUrl(String str): 登陆成功后转发,可以转发到静态页面 也可以转发到controller进行处理

successHandler(AuthenticationSuccessHandler authSuccess): 登陆成功后处理器,authSuccess 实现了AuthenticationSuccessHandler 接口,并实现其中唯一方法,该方法有三个参数,通过这三个参数就可返回相关信息

验证码的实现

  1. 创建验证码控制器,并实现验证码的生成,并将生成的验证码放入session中

    java
     1@RestController
     2@RequestMapping("/verification")
     3public class VerificationController {
     4    private static final int height=30;
     5    private static final int weight=150;
     6    private static final int codeSize=6;
     7
     8    private static final int codeSpace=24;
     9
    10    @RequestMapping("/code")
    11    public void code(HttpServletRequest request, HttpServletResponse response) throws IOException {
    12        //创建 BufferedImage 对象,参数为 宽、高、图像类型
    13        BufferedImage image=new BufferedImage(weight,height,BufferedImage.TYPE_INT_RGB);
    14
    15        //获取画笔,对图像进行绘制
    16        Graphics g = image.getGraphics();
    17
    18        //设置画笔颜色为白色
    19        g.setColor(Color.WHITE);
    20        //填充指定的矩形,参数:起始位置 x y、绘制大小 width height
    21        g.fillRect(0,0,weight,height);
    22
    23
    24        Random random = new Random();
    25        //存储验证码,后续方便放入session
    26        StringBuilder codeStr=new StringBuilder();
    27        //绘制验证码
    28        for (int i=0;i<codeSize;i++){
    29            //设置颜色
    30            g.setColor(new Color(
    31                    random.nextInt(255),random.nextInt(255),random.nextInt(255)
    32            ));
    33            //设置字体 字体构造参数:字体、风格、大小
    34            g.setFont(new Font("宋体",Font.BOLD,24));
    35            //绘制字符串 参数:字符、x、y
    36            String code = RandomUtil.randomString(1);
    37            codeStr.append(code);
    38            g.drawString(code,codeSpace*i,random.nextInt(height/2)+height/2);
    39        }
    40
    41        //将生成的验证码字符串放入session
    42        request.getSession().setAttribute("codeStr",codeStr.toString());
    43
    44        //绘制干扰线
    45        for (int i=0;i<4;i++){
    46            g.setColor(new Color(
    47                    random.nextInt(255),random.nextInt(255),random.nextInt(255)
    48            ));
    49            //绘制干扰线 参数:线的起点 x1,y1、线的终点 x2,y2
    50            g.drawLine(random.nextInt(weight/2), random.nextInt(height),
    51                    random.nextInt(weight), random.nextInt(height));
    52        }
    53
    54        //告诉浏览器响应内容格式
    55        response.setContentType("image/png");
    56        //告诉浏览器不要进行缓存
    57        response.setHeader( "Pragma", "no-cache" );
    58        response.setHeader( "Cache-Control", "no-cache" );
    59        response.setDateHeader("Expires",0);
    60
    61        ServletOutputStream out = response.getOutputStream();
    62        //通过 ImageIO 工具类将图片写入 response 输出流中
    63        ImageIO.write(image,"png",out);
    64        out.flush();
    65        out.close();
    66    }
    67}
  2. 将验证码的请求地址放行

    java
    1http.authorizeRequests()    //允许RequestMatcher进行权限控制
    2                .antMatchers("/index","/login.html","/index.html","/**/**.js","/verification/**").permitAll()      //该地址放行
    3    //..........
  3. 前端页面和js设置

    java
     1<body>
     2<h1>登录页</h1>
     3    用户名<input type="text" name="username"><br>
     4    密码<input id="password" type="password" name="password"><br>
     5    验证码<input id="code" type="text" name="code">
     6    <img src="/verification/code" id="imgCode">
     7    <a href="javascript:void(0)" onclick="changeCode()">重新获取</a>
     8    <button id="loginBtn"> 登录 </button>
     9</body>
    10
    11<script>
    12    $(document).ready(function(){
    13        $("#loginBtn").on("click",function (){
    14            $.ajax({
    15                url:'/login',
    16                method:'post',
    17                data:{
    18                    username:$('input[name="username"]').val(),
    19                    password:$('input[name="password"]').val(),
    20                    code:$('input[name="code"]').val()
    21                },
    22                success:(res)=>{
    23                    if (res.code !== 0){
    24                        alert("登陆失败:"+res.msg)
    25                    }else{
    26                        alert("登陆成功!!")
    27                    }
    28                },
    29            })
    30        })
    31    });
    32    function changeCode(){
    33        //对img标签的src重新赋值,可以实现图片的重新加载
    34        $("#imgCode").attr("src","/verification/code?t="+new Date())
    35    }
    36</script>
  4. 设置过滤器用于验证码的校验,参考后面自定义过滤器

自定义过滤器

Spring Security 框架是使用过滤器实现的

例如:验证用户名密码是否正确 :UsernamePasswordAuthenticationFilter 过滤器

实现过滤器的方式

  • 直接实现 javax.servlet.Filter
  • 继承spring框架提供的一些过滤器抽象类;例如:org.springframework.web.filter.OncePerRequestFilter 该过滤器确保了一次请求只执行一次该过滤器

注意:设置字符编码的代码必须在 获取输出对象之前,否则无效

验证码校验过滤器实现

java
 1@Component
 2public class VerificationCodeFilter extends OncePerRequestFilter {
 3
 4    @Override
 5    protected void doFilterInternal(HttpServletRequest request,
 6                                    HttpServletResponse response,
 7                                    FilterChain filterChain) throws ServletException, IOException {
 8        //如果非登录请求直接放行
 9        if(!"/login".equals(request.getRequestURI())){
10            filterChain.doFilter(request,response);
11        }else{
12            try {
13                verification(request);
14                filterChain.doFilter(request,response);
15            }catch (AuthenticationException e){
16                response.setContentType("application/json;charset=utf-8");
17                response.setCharacterEncoding("utf-8");
18                //这里定义登陆失败处理handler 需要实现 AuthenticationFailureHandler 接口,也可以通过response直接处理,
19                PrintWriter out = response.getWriter();
20                //使用jackson写入json对象
21                ObjectMapper om=new ObjectMapper();
22                om.writeValue(out, Map.of("code",-1,"msg", e.getMessage()));
23                out.flush();
24                out.close();
25            }
26        }
27    }
28
29    /**
30     * 验证 验证码
31     */
32    private void verification(HttpServletRequest request) throws AuthenticationException {
33        HttpSession session = request.getSession();
34        //请求中验证码
35        String codeReq=request.getParameter("code");
36        //session中存的请求
37        String codeSession=String.valueOf(session.getAttribute("codeStr"));
38
39        //如果session中存在验证码值,清除session中验证码
40        if (!ObjectUtil.equals("null",codeSession)){
41            session.removeAttribute("codeStr");
42        }
43
44        //输入验证码和session中验证码不等
45        if (!ObjectUtil.equals(codeReq,codeSession)){
46            throw new AuthenticationException("验证码校验失败!!!", new RuntimeException("校验失败")) {
47                private static final long serialVersionUID = 6982564443630627045L;
48            };
49        }
50    }
51}

添加过滤器到过滤器链

java
1@Resource
2private VerificationCodeFilter verificationCodeFilter;
3@Override
4protected void configure(HttpSecurity http) throws Exception {
5	http.authorizeRequests()    //允许RequestMatcher进行权限控制
6                .addFilterBefore(verificationCodeFilter,UsernamePasswordAuthenticationFilter.class)     //添加过滤器
7}
使用滚轮缩放
按住拖动