SpringBoot SpringSecurity

SpringBoot SpringSecurity (Spring Date Jpa + SpringSecurity)

一、简介

 SpringSecurity是专门针对基于Spring的项目的安全框架,充分利用了依赖注入和AOP来实现安全的功能。

安全框架有两个重要的概念,即认证(Authentication)和授权(Authorization)。认证即确认用户可以访问当前系统;授权即确定用户在当前系统下所有的功能权限。

二、框架原理

对Web资源进行保护,充分运用过滤器、拦截器、AOP。SpringSecurity在我们进行用户认证以及授予权限的时候,通过各种各样的拦截器、过滤来控制权限的访问,从而实现安全。
        如下为其主要过滤器  :

  •         WebAsyncManagerIntegrationFilter 
  •         SecurityContextPersistenceFilter 
  •         HeaderWriterFilter 
  •         CorsFilter 
  •         LogoutFilter
  •         RequestCacheAwareFilter
  •         SecurityContextHolderAwareRequestFilter
  •         AnonymousAuthenticationFilter
  •         SessionManagementFilter
  •         ExceptionTranslationFilter
  •         FilterSecurityInterceptor
  •         UsernamePasswordAuthenticationFilter
  •         BasicAuthenticationFilter

三、框架的核心组件

  •       SecurityContextHolder:提供对SecurityContext的访问
  •       SecurityContext,:持有Authentication对象和其他可能需要的信息
  •       AuthenticationManager 其中可以包含多个AuthenticationProvider
  •       ProviderManager对象为AuthenticationManager接口的实现类
  •       AuthenticationProvider 主要用来进行认证操作的类 调用其中的authenticate()方法去进行认证操作
  •       Authentication:Spring Security方式的认证主体
  •       GrantedAuthority:对认证主题的应用层面的授权,含当前用户的权限信息,通常使用角色表示
  •      UserDetails:构建Authentication对象必须的信息,可以自定义,可能需要访问DB得到
  •       UserDetailsService:通过username构建UserDetails对象,通过loadUserByUsername根据userName获取UserDetail对象 (可以在这里基于自身业务进行自定义的实现  如通过数据库,xml,缓存获取等)

四、Maven依赖

        <!-- springsecurity 硬性依赖 begin 至于是springsecurity5还是springsecurity4取决于SpringBoot工程的版本 -->
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        </dependency>

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

        <!-- springsecurity 硬性依赖 end -->

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

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- mybatis+plus -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.1.1</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.4.4</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.0.7.1</version>
        </dependency>

        <!-- MySQL Driver -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.45</version>
            <scope>runtime</scope>
        </dependency>
        <!-- Druid Pool -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.9</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>

五、yml配置:

server:
  port: 8080

spring:
  datasource:
    name: druidDataSource
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://ip:port/dbname?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&failOverReadOnly=false
    username: root
    password: root

#  resources:
#    # 静态资源配置
#    static-locations: classpath:/static/

  jpa:
    show-sql: true
    hibernate:
      ddl-auto: none

  thymeleaf:
    cache: false
    #模板配置

mybatis-plus:
  mapper-locations: classpath*:com/example/mapper**/*Mapper.xml
  configuration:
    map-underscore-to-camel-case: true

六、SQL创建语句(MySQL:user、role、user_roles,角色命名如:ROLE_ADMIN、ROLE_USER等):

/*
Navicat MySQL Data Transfer

Source Server         : mysql(11.53.56.70)
Source Server Version : 50726
Source Host           : 11.53.56.70:3306
Source Database       : scps3

Target Server Type    : MYSQL
Target Server Version : 50726
File Encoding         : 65001

Date: 2019-12-11 15:41:54
*/

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  `username` varchar(255) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=53 DEFAULT CHARSET=utf8;


/*
Navicat MySQL Data Transfer

Source Server         : mysql(11.53.56.70)
Source Server Version : 50726
Source Host           : 11.53.56.70:3306
Source Database       : scps3

Target Server Type    : MYSQL
Target Server Version : 50726
File Encoding         : 65001

Date: 2019-12-11 15:41:32
*/

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(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;


/*
Navicat MySQL Data Transfer

Source Server         : mysql(11.53.56.70)
Source Server Version : 50726
Source Host           : 11.53.56.70:3306
Source Database       : scps3

Target Server Type    : MYSQL
Target Server Version : 50726
File Encoding         : 65001

Date: 2019-12-11 15:41:41
*/

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for user_roles
-- ----------------------------
DROP TABLE IF EXISTS `user_roles`;
CREATE TABLE `user_roles` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL,
  `roles_id` int(11) NOT NULL,
  PRIMARY KEY (`id`,`user_id`,`roles_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

七、java配置:

1、实体类(User、Role),User继承UserDetails接口,注意配置User与Role的关系与getAuthorities()方法重写;

package com.example.domain;

import javax.persistence.*;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

@Entity
public class User implements Serializable,UserDetails{

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private Integer age;
    private String username;
    private String password;

    @ManyToMany(cascade = {CascadeType.REFRESH},fetch = FetchType.EAGER)
    private List<Role> roles;

    public User() {
        super();
    }

    public Long getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getUsername() {
        return username;
    }

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

    public String getPassword() {
        return password;
    }

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

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

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

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", roles=" + roles +
                '}';
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        ArrayList<GrantedAuthority> auths = new ArrayList<>();
        List<Role> roles = this.getRoles();
        for (Role role : roles) {
            auths.add( new SimpleGrantedAuthority( role.getName() ) );
        }
        return auths;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}
package com.example.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.io.Serializable;

@Entity
public class Role implements Serializable {

    private static final long serialVersionUID = 2L;

    @Id
    @GeneratedValue
    private Long id;
    private String name;

    public Role() {
        super();
    }

    public Long getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Role{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

2、Jpa中提供查询方法:

import com.example.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User,Long> {

    User findByUsername (String username);
}

3、自定义UserDetailsService实现类:/login会默认请求loadUserByUsername()方法

package com.example.service;

import com.example.dao.UserRepository;
import com.example.domain.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;

public class CustomUserService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        User user = this.userRepository.findByUsername( username );
        if (user == null)
            throw new UsernameNotFoundException( "用户名不存在!" );
        return user;
    }
}

4、自定义拦截器:未通过身份验证的请求都转发到/login页面

package com.example.config;

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

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController( "/login" ).setViewName( "login" );//拦截所有请求跳转至/login.html
    }

}

5、PasswordEncoder配置:(密码加密配置),之前不需要配置,现在涉及到密码加密

  • 新版本中如果没有关于密码加密配置,
    会报错java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null",如图:

package com.example.config;

import org.springframework.security.crypto.password.PasswordEncoder;

public class PasswordConfig implements PasswordEncoder {

    @Override
    public String encode(CharSequence charSequence) {
        return charSequence.toString();
    }

    @Override
    public boolean matches(CharSequence charSequence, String s) {
        return s.equals( charSequence.toString() );
    }
}

6、SpringSecurity配置:

  • 开启@EnableWebSecurity;

  • @EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)开启方法控制注解 @Secured、@PreAuthorize、@PostAuthorize;

  • 数据源配置customUserService()、configure(AuthenticationManagerBuilder auth);

  • 访问规则配置configure(HttpSecurity http);

  • 静态资源配置configure(WebSecurity web);

package com.example.config;

import com.example.service.CustomUserService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)//开启方法控制注解 @Secured、@PreAuthorize、@PostAuthorize
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    UserDetailsService customUserService(){
        return new CustomUserService();//注入对接业务层
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService( customUserService() ).passwordEncoder( new PasswordConfig() );//注入数据源,自定义userDetailService认证
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()//开启认证通过访问
                .and()
                .formLogin()
                .loginPage( "/login" )//登录界面
//                .defaultSuccessUrl( "/index.html" )//这里指定的是静态页面,必须加后缀,如果不指定,就走路径为“/”的方法
                .failureUrl( "/login?error" )
                .permitAll()//登录成功任意访问
                .and()
                .logout().permitAll();//注销行为请求任意访问

    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        //设置静态资源不要拦截
        web.ignoring().antMatchers("/js/**","/cs/**","/images/**");
    }
}

7、html模板login.html、index.html

  • thymeleaf、springsecurity5声明,使用thymeleaf模板语言和springsecurity5权限标签必须开启声明;
  • form提交路径请求loadUserByUsername()认证方法默认路径/login,属性th:action="@{/login}" action="/login",post请求;
  • 默认提交属性username、password;
  • 默认登出请求路径/logout 属性th:action="@{/logout}" post请求;

注:这里如果属性不对应会导致请求loadUserByUsername()方法参数接收不到,请求路径不对应则不会请求loadUserByUsername()方法

模板样例:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"><!--thymeleaf声明-->
<head>
    <meta content="text/html;charset=UTF-8">
    <title>Login</title>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
    <div class="container">
        <div class="navbar-header">
            <a class="navbar-brand" href="#">Spring Security演示</a>
        </div>
        <div id="navbar" class="collapse navbar-collapse">
            <ul class="nav navbar-nav">
                <li><a th:href="@{/}">首页</a></li>
            </ul>
        </div>
    </div>
</nav>
<div class="container">
    <div class="starter-template">
        <p th:if="${param.logout}" class="bg-warning">已成功注销</p>
        <p th:if="${param.error}" class="bg-danger">有错误,请重试</p>
        <h2>使用账号密码登录</h2>
        <form name="form" th:action="@{/login}" action="/login" method="post">
            <div class="form-group">
                <label for="username">账号</label>
                <input type="text" class="form-control" name="username" value="" placeholder="账号">
            </div>
            <div class="form-group">
                <label for="password">密码</label>
                <input type="text" class="form-control" name="password" value="" placeholder="密码">
            </div>
            <input type="submit" id="login" value="Login" class="btn">
        </form>
    </div>
</div>
</body>
</html>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"><!--web权限标签声明-->
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>Index</title>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
    <div class="container">
        <div class="navbar-header">
            <a class="navbar-brand" href="#">Spring Security演示</a>
        </div>
        <div id="navbar" class="collapse navbar-collapse">
            <ul class="nav navbar-nav">
                <li><a th:href="@{/}">首页</a></li>
            </ul>
        </div>
    </div>
</nav>
<div class="container">
    <div class="starter-template">
        <h1 th:text="${msg.title}"></h1>
        <p class="bg-primary" th:text="${msg.content}"></p>

        <div sec:authorize="hasRole('ROLE_ADMIN')">
            <p class="bg-info">管理员能查看的信息</p>
        </div>
        <div sec:authorize="hasRole('ROLE_USER')">
            <p class="bg-info">用户能查看的信息</p>
        </div>
        <form th:action="@{/logout}" method="post">
            <input type="submit" class="btn btn-primary" value="注销" >
        </form>
    </div>
</div>
</body>
</html>

8、默认控制器:(控制登录成功后页面的跳转),其中Msg是自定义实体类

package com.example.controller;

import com.example.domain.Msg;
import com.example.domain.User;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class HomeController {

    @RequestMapping("/")
    public String index (@AuthenticationPrincipal UserDetails userDetails, Model model){
        Msg msg = new Msg( "测试标题", "测试内容", "额外信息,只有管理员可以看到!" );
        model.addAttribute( "msg",msg );
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        System.out.println( authentication );
        System.out.println( userDetails);
        System.out.println("---------------------------");
        System.out.println( authentication.getDetails() );
        System.out.println( authentication.getAuthorities() );
        System.out.println( authentication.getCredentials());
        System.out.println( authentication.getPrincipal());
        System.out.println("------------------------------");
        /**
         * authentication.getPrincipal() 可以强转为登录对象获取登录信息
         */
        User user =(User) authentication.getPrincipal();
        System.out.println("authentication.getPrincipal():获取登录对象信息"+user.getPassword());
        String[] split = authentication.getDetails().toString().split( ";" );
        String[] split1 = split[0].split( ":" );
        String remoteIpAddress = "";
        for (int i = 0; i < split1.length; i++) {
            if (i > 1){
                remoteIpAddress += split1[i];
            }
            if (i > 2 || i == split1.length){
                remoteIpAddress += ":";
            }
        }
        String sessionId = split[1].split( ":" )[1];
        /**
         * authentication.getDetails() 获取登录绑定式信息
         *  SessionId、RemoteIpAddress
         */
        System.out.println("获取登录绑定信息:"+remoteIpAddress + sessionId);
        /**
         * 这里可以根据 switch case 来判断 authentication.getAuthorities()
         * 区分最会跳转的页面
         */
        System.out.println("获取登录角色:"+authentication.getAuthorities());
        return "/index";
    }

    /**
     * 访问限制
     * hasRole和hasAuthority 两种性质完成独立的东西
     * 一个是用做角色控制,一个是操作权限的控制,二者也并不矛盾
     *
     * @GetMapping("/user-role")
     *   @PreAuthorize("hasRole('USER')")
     *   public String readUser() {
     *     return "have a user role";
     *   }
     *
     *   @PreAuthorize("hasAuthority('write')")
     *
     *   @PreAuthorize("hasAuthority('read')")
     *
     *   @PreAuthorize("hasAnyAuthority('read','write')")
     *
     *   @PreAuthorize("hasRole('admin')")
     */


}

Msg自定义实体类:(无伤大雅)

package com.example.domain;

import java.io.Serializable;

public class Msg implements Serializable {

    private static final Long serialVersionUID = 3L;
    private String title;
    private String content;
    private String etraInfo;

    public Msg(String title, String content, String etraInfo) {
        super();
        this.title = title;
        this.content = content;
        this.etraInfo = etraInfo;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public String getEtraInfo() {
        return etraInfo;
    }

    public void setEtraInfo(String etraInfo) {
        this.etraInfo = etraInfo;
    }

    @Override
    public String toString() {
        return "Msg{" +
                "title='" + title + '\'' +
                ", content='" + content + '\'' +
                ", etraInfo='" + etraInfo + '\'' +
                '}';
    }
}

9、测试截图 

登录界面:

普通用户登录:

管理员登录:

注销:

错误:

chenyb 随笔记录,只为方便自己学习

2019-12-11

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值