Spring Boot 安全管理

Spring Boot 安全管理

Spring Security与Shiro简介

Java开发常用的安全框架有Shiro和Spring Security。Shiro是一个轻量级的安全管理框架,用于认证、授权、会话管理、密码管理、缓存管理等功能,Spring Security源自于Spring家族,而且Spring Boot提供了自动化配置,可以与Spring框架无缝衔接,与Shiro相比要复杂,但是功能更加强大,权限控制更加详细。

Spring Security

POM依赖文件及数据库连接

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
        
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.0.9</version>
        </dependency>
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/security
spring.data.mongodb.username=root
spring.datasource.password=123456

基础配置

Spring Boot为Spring Security提供了自动化配置,我们只要简单开发就可以使用了。
1、创建项目,添加依赖配置
创建一个Spring Boot Web项目,添加Spring Security和Web依赖

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

2、创建接口

@RestController
public class SpringSecurityTestController {
    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}

3、浏览器访问localhost:8080/hello,hello接口会自动调转到Spring Security提供的登录界面:localhost:8080/login
在这里插入图片描述
默认的用户名为user,密码是随机生成的,在控制台中显示。
在这里插入图片描述

用户名、密码配置

如果想要指定用户名、密码、用户角色,需要在application.properties文件中配置

spring.security.user.name=sang
spring.security.user.password=123
spring.security.user.roles=admin

内存认证

内存认证自定义一个java类,继承WebSecurityConfigurerAdapter接口,重写configure方法。

/**
 * 自定义MyWebSercurityConfig继承WebSecurityConfigurerAdapter
 * 实现基于内存的认证
 */
@Configuration
public class MyWebSercurityConfig extends WebSecurityConfigurerAdapter {
    //    确认密码的加密方式
    @Bean
    PasswordEncoder passwordEncoder() {
    //	  使用不对密码加密形式
        return NoOpPasswordEncoder.getInstance();
    }

    /**
     * 重写configure方法,定义三个用户和角色
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().
                withUser("admin").password("123").roles("ADMIN", "USER").and()
                .withUser("sang").password("123").roles("USER").and()
                .withUser("root").password("123").roles("ADMIN", "DBA");
    }
}

首先MyWebSercurityConfig继承了WebSecurityConfigurerAdapter接口,采用了密码不加密形式,然后重写了configure(AuthenticationManagerBuilder auth)方法,在方法中定义了三个用户信息:
账号:admin 密码:123 角色:ADMIN、USER
账号:sang 密码:123 角色:USER
账号:root 密码:123 角色:ADMIN、DBA

HttpSecurity

到此认证功能可以实现,但是受到保护的资源都是默认的,无法根据实际情况调整,如果想要实现就需要重写WebSecurityConfigurerAdapter中的configure(HttpSecurity http)方法。

 @Override
    protected void configure(HttpSecurity http) throws Exception {
//        开启HttpSecurity配置
        http.authorizeRequests()
//                用户访问“/admin/**”模式的URL必须具备ADMIN的角色
                .antMatchers("/admin/**")
                .hasRole("ADMIN")
                //                用户访问“/user/**”模式的URL必须具备ADMIN或USER的角色
                .antMatchers("/user/**")
                .access("hasAnyRole('ADMIN','USER')")
                //                用户访问“/dba/**”模式的URL必须具备ADMIN和DBA的角色
                .antMatchers("/db/**")
                .access("hasRole('ADMIN')and hasRole('DBA')")
//              除了前面定义的URL模式外,用户访问其它的URL都需要认证成功后才能访问
                .anyRequest()
                .authenticated()
                .and()
//                开启表单登录,即用户直接看到登录界面,同时配置登录接口为“/login”,
//                发起post登录请求,登录参数用户名必须为username,密码必须为password,
//                配置loginProcessingUrl接口主要方便Ajax或者移动端调用登录接口
                .formLogin()
                .loginProcessingUrl("/login")
//                和登录相关的接口都不需要认证即可访问
                .permitAll()
                .and()
                .csrf()
                .disable();
    }

创建测试接口

@RestController
public class SpringSecurityTestController {

    @GetMapping("/admin/hello")
    public String admin() {
        return "hello admin";
    }

    @GetMapping("/user/hello")
    public String user() {
        return "hello user";
    }

    @GetMapping("/db/hello")
    public String dba() {
        return "hello dba";
    }
}

使用浏览器访问不同的接口,返回的数据也不同,这里就不再演示了。

登录表单详细配置

注:23-84行代码

@Override
    protected void configure(HttpSecurity http) throws Exception {
//        开启HttpSecurity配置
        http.authorizeRequests()
//                用户访问“/admin/**”模式的URL必须具备ADMIN的角色
                .antMatchers("/admin/**")
                .hasRole("ADMIN")
//                用户访问“/user/**”模式的URL必须具备ADMIN或USER的角色
                .antMatchers("/user/**")
                .access("hasAnyRole('ADMIN','USER')")
//                用户访问“/db/**”模式的URL必须具备ADMIN和DBA的角色
                .antMatchers("/db/**")
                .access("hasRole('ADMIN')and hasRole('DBA')")
//              除了前面定义的URL模式外,用户访问其它的URL都需要认证成功后才能访问
                .anyRequest()
                .authenticated()
                .and()
//                开启表单登录,即用户直接看到登录界面,同时配置登录接口为“/login”,
//                发起post登录请求,登录参数用户名必须为username,密码必须为password,
                .formLogin()
//                配置登录界面,然后用户访问需要授权的页面,会自动跳转到login_page页面让用户登录,
//                这里的登录页面是开发者自定义的页面,不是默认登录页面
                .loginPage("/login_page")
//                配置loginProcessingUrl接口主要方便Ajax或者移动端调用登录接口
                .loginProcessingUrl("/login")
//                定义用户名、密码的参数名,默认的参数名是username、password
                .usernameParameter("name")
                .passwordParameter("passwd")
//                定义登录成功的业务逻辑,用户登录成功后可以跳转到某个页面,
//				  也可以返回json数据,此处返回json文件
                .successHandler(new AuthenticationSuccessHandler() {
//                onAuthenticationSuccess中的auth参数获取用户信息,
//                在登录成功后,将用户信息返回给客户端
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest req,
                                                        HttpServletResponse resp,
                                                        Authentication auth)
                            throws IOException, ServletException {
                        Object principal = auth.getPrincipal();
                        resp.setContentType("applicaotin/json;charset=utf-8");
                        PrintWriter out = resp.getWriter();
                        resp.setStatus(200);
                        Map<String, Object> map = new HashMap<>();
                        map.put("status", 200);
                        map.put("msg", principal);
                        ObjectMapper om = new ObjectMapper();
                        out.write(om.writeValueAsString(map));
                        out.flush();
                        out.close();
                    }
                })
//                处理登录失败的业务逻辑
                .failureHandler(new AuthenticationFailureHandler() {
//                登录失败会回调onAuthenticationFailure的AuthenticationException参数
//                ,给用户一个登录失败的提示
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest rep,
                                                        HttpServletResponse resp,
                                                        AuthenticationException e)
                            throws IOException, ServletException {
                        resp.setContentType("applicaotin/json;charset=utf-8");
                        PrintWriter printWriter = resp.getWriter();
                        resp.setStatus(401);
                        Map<String, Object> map = new HashMap<>();
                        map.put("status", 401);
                        if (e instanceof LockedException) {
                            map.put("msg", "账号被锁定,登录失败!");
                        } else if (e instanceof BadCredentialsException) {
                            map.put("msg", "账户名或密码输入错误,登录失败!");
                        } else if (e instanceof DisabledException) {
                            map.put("msg", "账户被禁用,登录失败!");
                        } else if (e instanceof AccountExpiredException) {
                            map.put("msg", "账户已过期,登录失败!");
                        } else if (e instanceof CredentialsExpiredException) {
                            map.put("msg", "密码已过期,登录失败!");
                        } else {
                            map.put("msg", "登录失败!");
                        }
                        ObjectMapper objectMapper = new ObjectMapper();
                        printWriter.write(objectMapper.writeValueAsString(map));
                        printWriter.flush();
                        printWriter.close();
                    }
                })
//                和登录相关的接口都不需要认证即可访问
                .permitAll()
                .and()
                .csrf()
                .disable();
    }

测试方法如下:(使用Postman测试接口)
在这里插入图片描述

在这里插入图片描述

注销登录配置

                .and()
//                注销配置
                .logout()
//                注销登录的URL请求设置
                .logoutUrl("logout")
//                清除身份认证信息
                .clearAuthentication(true)
//                使Session失效
                .invalidateHttpSession(true)
//                开发者可以在addLogoutHandler配置数据清除工作,如Cookoe
                .addLogoutHandler(new LogoutHandler() {
                    @Override
                    public void logout(HttpServletRequest rep,
                                       HttpServletResponse resp,
                                       Authentication authentication) {
                    }
                })
//                注销成功后的业务逻辑配置
                .logoutSuccessHandler(new LogoutSuccessHandler() {
                    @Override
                    public void onLogoutSuccess(HttpServletRequest rep,
                                                HttpServletResponse resp,
                                                Authentication authentication)
                            throws IOException, ServletException {
//                        返回json数据或者页面
                        resp.sendRedirect("/login_page");
                    }
                })
                .and()

多个HttpSecurity

如果创建多个HttpSecurity,父类HttpSercurityS 不需要继承WebSecurityConfigurerAdapter,而是在HttpSercurityS类中创建静态内部子类去继承WebSecurityConfigurerAdapter,并且在子类上添加@Configuration和@Order注解
注:
@Order(1):配置优先级,数字越小优先级越大,未加注解的优先级最小

package com.example.demo.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class HttpSercurityS {
    @Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Autowired
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().
                withUser("admin").password("123").roles("ADMIN", "USER").and()
                .withUser("sang").password("123").roles("USER").and()
                .withUser("root").password("123").roles("ADMIN", "DBA");
    }

    @Configuration
    @Order(1)
    public static class AdminSecurityConfig extends WebSecurityConfigurerAdapter {
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .antMatchers("/admin/**")
                    .hasRole("ADMIN")
                    .antMatchers("/user/**")
                    .access("hasAnyRole('ADMIN','USER')")
                    .antMatchers("/db/**")
                    .access("hasRole('ADMIN')and hasRole('DBA')");
        }
    }

    @Configuration
    public static class OtherSecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity httpSecurity) throws Exception {
            httpSecurity.authorizeRequests()
                    .anyRequest().fullyAuthenticated()
                    .and()
                    .formLogin()
                    .loginProcessingUrl("/others")
                    .permitAll()
                    .and()
                    .csrf().
                    disable();
        }
    }
}


密码加密

为了保证用户的信息安全,开发者往往会给信息使用各种加密。密码加密一般会用到散列函数,又称散列算法、哈希函数,这是一种从任何数据中创建数字“指纹”的方法,但是仅仅使用散列函数还是不够,需要在加密过程中添加“盐”,这里的盐是一个随机数,加盐后即使密码明文相同的用户生成的密码,密文也不同,这大大提高了用户信息的安全。
Spring Security提供了多种加密方案,官方推荐使用BCryptPasswordEncoder,BCryptPasswordEncoder使用BCrypt强哈希函数,开发者在使用时可以选择strength和SecureRandom实例。strength越大,密钥迭代次数越多,密钥迭代次数为2^strength。strength取值在4-31之间,默认为10。
使用BCrypt加密算法步骤如下:

package com.example.demo.config;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

/**
 * BCrypt哈希加密获取
 */
public class GetBCryptPasswordEncoder {
    public static void main(String[] args) {
//        密码明文
        String password = "123456";
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String hashedPassword = passwordEncoder.encode(password);
        System.out.println(hashedPassword);
    }
}

密文:
在这里插入图片描述
修改passwordEncoder的加密方式:

  @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(10);
    }

将加密后的密码替换上:

 @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
      auth.inMemoryAuthentication().
      withUser("admin")
      //password("123")
      .password("$2a$10$BYGJlB7c2EqrZk68IdQr5ecRspJgvUKj0DFVPmRlRY6uZ0TCZwHwS")
      .roles("ADMIN", "USER");
    }

方法安全

因为Spring Security与spring无缝衔接,Spring还提供了注解的形式来灵活配置,省去了URL配置的繁琐。需要@EnableGlobalMethodSecurity去开启注解的安全配置。

package com.example.demo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;

/**
 * prePostEnabled = true解锁@PreAuthorize()、@PostAuthorize()
 * PreAuthorize():在方法执行前进行验证
 * PostAuthorize():在方法执行后进行验证
 * securedEnabled = true:解锁@Secured
 */
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfig {
}

角色权限设置:

package com.example.demo.service;

import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;

@Service
public class WebSecurityConfigServices {
    //    访问admin需要ADMIN角色
    @Secured("ROLE_ADMIN")
    public String admin() {
        return "hello admin";
    }

    //访问dba需要ADMIN和DBA角色
    @PreAuthorize("hasRole('ADMIN')and hasRole('DBA')")
    public String dba() {
        return "hello dba";
    }

    //    访问user需要ADMIN、DBA、USER三个其中一个角色
    @PreAuthorize("hasAnyRole('ADMIN','DBA','USER')")
    public String user() {
        return "hello user";
    }
}

数据库认证

sql语句:创建表,添加数据



SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) DEFAULT NULL,
  `nameZh` varchar(32) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES ('1', 'dba', '数据库管理员');
INSERT INTO `role` VALUES ('2', 'admin', '系统管理员');
INSERT INTO `role` VALUES ('3', 'user', '用户');

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(32) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  `enabled` tinyint(1) DEFAULT NULL,
  `locked` tinyint(1) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', 'root', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq', '1', '0');
INSERT INTO `user` VALUES ('2', 'admin', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq', '1', '0');
INSERT INTO `user` VALUES ('3', 'sang', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq', '1', '0');

-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `uid` int(11) DEFAULT NULL,
  `rid` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES ('1', '1', '1');
INSERT INTO `user_role` VALUES ('2', '1', '2');
INSERT INTO `user_role` VALUES ('3', '2', '2');
INSERT INTO `user_role` VALUES ('4', '3', '3');
SET FOREIGN_KEY_CHECKS=1;

创建实体类:角色表、用户表

public class Role {
    private Integer id;
    private String name;
    private String nameZh;
}
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public class User implements UserDetails {
    private Integer id;
    private String username;
    private String password;
    private Boolean enabled;
    private Boolean locked;
    private List<Role> roles;

    //    获取用户所具有的角色信息
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        for (Role role : roles
        ) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }

    //获取用户密码
    @Override
    public String getPassword() {
        return password;
    }

    //获取用户名
    @Override
    public String getUsername() {
        return username;
    }

    //当前账户是否为过期
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    //当前账户是否未锁定
    @Override
    public boolean isAccountNonLocked() {
        return !locked;
    }

    //当前账户密码是否未过期
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    //当前账户是否可用
    @Override
    public boolean isEnabled() {
        return enabled;
    }


    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }


    public void setUsername(String username) {
        this.username = username;
    }


    public void setPassword(String password) {
        this.password = password;
    }

    public Boolean getEnabled() {
        return enabled;
    }

    public void setEnabled(Boolean enabled) {
        this.enabled = enabled;
    }

    public Boolean getLocked() {
        return locked;
    }

    public void setLocked(Boolean locked) {
        this.locked = locked;
    }

    public List<Role> getRoles() {
        return roles;
    }

    public void setRoles(List<Role> roles) {
        this.roles = roles;
    }
}

```java
package com.example.demo.service;

import com.example.demo.Mapper.UserMapper;
import com.example.demo.bean.User;
import org.springframework.beans.factory.annotation.Autowired;
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;

@Service
public class UserService implements UserDetailsService {
    @Autowired
    UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.loadUserByUsername(username);
        if (user == null) throw new UsernameNotFoundException("账户不存在!");
        user.setRoles(userMapper.getUserRolesByUid(user.getId()));

        return user;
    }
}

<?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.example.demo.Mapper.UserMapper">
    <select id="loadUserByUsername" resultType="com.example.demo.bean.User">
        select * from user where username=#{username}
    </select>
    <select id="getUserRolesByUid" resultType="com.example.demo.bean.Role">
        select * from role r,user_role ur where r.id=ur.rid and ur.uid=#{id}
    </select>
</mapper>

## 备注
本人在学习中整理的文档,仅供参考。如有错误请留言指出,谢谢。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值