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