Spring Security 入门系列
1.项目目录结构
2.pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com</groupId>
<artifactId>security</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>security</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</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>
<dependency>
<groupId>net.sourceforge.nekohtml</groupId>
<artifactId>nekohtml</artifactId>
<version>1.9.22</version>
</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>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3.application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/security
driver-class-name: com.mysql.jdbc.Driver
username: root
password: 123456
jpa:
show-sql: true
hibernate:
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
thymeleaf:
suffix: .html
cache: false
mode: LEGACYHTML5
prefix: classpath:/templates/
4.实体类Permission
package com.cxb.security.entity;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@Entity
@Table(name = "Sys_permission", schema = "springsecurity", catalog = "")
public class Permission {
private int id;
private String name;
private String description;
private String url;
private Integer pid;
private List<Role> roles = new ArrayList<>();
@ManyToMany(mappedBy = "permissions")
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
@Id
@Column(name = "id")
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
@Basic
@Column(name = "name")
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Basic
@Column(name = "description")
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
@Basic
@Column(name = "url")
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
@Basic
@Column(name = "pid")
public Integer getPid() {
return pid;
}
public void setPid(Integer pid) {
this.pid = pid;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Permission that = (Permission) o;
return id == that.id &&
Objects.equals(name, that.name) &&
Objects.equals(description, that.description) &&
Objects.equals(url, that.url) &&
Objects.equals(pid, that.pid);
}
@Override
public int hashCode() {
return Objects.hash(id, name, description, url, pid);
}
@Override
public String toString() {
return "Permission{" +
"id=" + id +
", name='" + name + '\'' +
", description='" + description + '\'' +
", url='" + url + '\'' +
", pid=" + pid +
'}';
}
}
5.实体类Role
package com.cxb.security.entity;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@Entity
@Table(name = "Sys_Role", schema = "springsecurity", catalog = "")
public class Role {
private int id;
private String name;
private List<User> users = new ArrayList<>();
private List<Permission> permissions = new ArrayList<>();
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "Sys_permission_role", joinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "permission_id", referencedColumnName = "id"))
public List<Permission> getPermissions() {
return permissions;
}
public void setPermissions(List<Permission> permissions) {
this.permissions = permissions;
}
@ManyToMany(mappedBy = "roles")
public List<User> getUsers() {
return users;
}
public void setUsers(List<User> users) {
this.users = users;
}
@Id
@Column(name = "id")
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
@Basic
@Column(name = "name")
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Role role = (Role) o;
return id == role.id &&
Objects.equals(name, role.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name);
}
@Override
public String toString() {
return "Role{" +
"id=" + id +
", name='" + name + '\'' +
", permissions=" + permissions +
'}';
}
}
6.User
package com.cxb.security.entity;
import javax.persistence.*;
import java.util.*;
@Entity
@Table(name = "Sys_User", schema = "springsecurity", catalog = "")
public class User {
private int id;
private String username;
private String password;
private List<Role> roles = new ArrayList<>();
/**
* @ManyToMany 表示多对多关系,fetch = FetchType.EAGER配置懒加载策略为立即加载,因为多对多涉及到树形结构的第二层,
* 使用懒加载会在使用roles对象时才去数据库查询,但是在本项目中会出现no session,暂时无法解决,所以加上次配置
*
* @JoinTable name:中间表名, @joinColumn : name:在中间表中对应外键名,referencedColumnName在原先表中的主键名
*
* inverseJoinColumns中的@joinColumn : name:多的另一方在中间表中对应的主键名,referencedColumnName在原先表中的主键名
*
* 此处的配置表明user和role的多对多关系由user维护
*/
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "Sys_Role_User", joinColumns = {@JoinColumn(name = "Sys_User_id", referencedColumnName = "id")},
inverseJoinColumns = {@JoinColumn(name = "Sys_Role_id", referencedColumnName = "id")})
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
@Id
@Column(name = "id")
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
@Basic
@Column(name = "username")
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Basic
@Column(name = "password")
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return id == user.id &&
Objects.equals(username, user.username) &&
Objects.equals(password, user.password);
}
@Override
public int hashCode() {
return Objects.hash(id, username, password);
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", roles=" + roles +
'}';
}
}
7.security核心配置
1).MyAccessDecisionManager
package com.cxb.security.security;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Service;
import java.util.Collection;
@Service
public class MyAccessDecisionManager implements AccessDecisionManager {
/**
* @param authentication UserService中循环添加到GrantedAuthority中的权限信息集合
* @param object 包含客户端发起的请求的request信息,可以转换为HTTPRequest
* @param collection url所需的权限集合
* @throws AccessDeniedException
* @throws InsufficientAuthenticationException
*/
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
//判断URL所需的权限集合是否为空,为空则放行
if (null == collection || collection.size() <= 0) {
return;
}
String needPermission;
for (ConfigAttribute c : collection) {
//获得所需的权限
needPermission = c.getAttribute();
//遍历用户拥有的权限进行对比
for (GrantedAuthority ga : authentication.getAuthorities()) {
if (needPermission.trim().equals(ga.getAuthority())){
return;
}
}
}
throw new AccessDeniedException("no permission");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
2).MyFilterSecurityInterceptor
package com.cxb.security.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.SecurityMetadataSource;
import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
import org.springframework.security.access.intercept.InterceptorStatusToken;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Service;
import javax.servlet.*;
import java.io.IOException;
@Service
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
@Autowired
private FilterInvocationSecurityMetadataSource securityMetadataSource;
//设置决策器
@Autowired
public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
super.setAccessDecisionManager(myAccessDecisionManager);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}
public void invoke(FilterInvocation fi) throws IOException, ServletException {
//fi里面有一个被拦截的url
//里面调用MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取fi对应的所有权限
//再调用MyAccessDecisionManager的decide方法来校验用户的权限是否足够
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
//执行下一个拦截器
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}
@Override
public void destroy() {
}
@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}
//添加判断url所需的权限类
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
}
3).MyInvocationSecurityMetadataSourceService
package com.cxb.security.security;
import com.cxb.security.dao.PermissionDao;
import com.cxb.security.entity.Permission;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
@Service
public class MyInvocationSecurityMetadataSourceService implements FilterInvocationSecurityMetadataSource {
@Autowired
private PermissionDao permissionDao;
private HashMap<String, Collection<ConfigAttribute>> map = null;
//在demo启动第一个用户登陆后,加载所有权限进map
public void loadResourceDefine() {
map = new HashMap<>();
Collection<ConfigAttribute> array;
ConfigAttribute cfg;
List<Permission> permissions = permissionDao.findAll();
for (Permission permission : permissions) {
array = new ArrayList<>();
//此处只添加了用户的名字,其实还可以添加更多权限的信息,例如请求方法到ConfigAttribute的集合中去。此处添加的信息将会作为MyAccessDecisionManager类的decide的第三个参数。
cfg = new SecurityConfig(permission.getName());
array.add(cfg);
//用权限的getUrl() 作为map的key,用ConfigAttribute的集合作为 value
map.put(permission.getUrl(), array);
}
}
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
if(map ==null) {
loadResourceDefine();
}
HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
AntPathRequestMatcher matcher;
//遍历权限表中的url
for (String url : map.keySet()) {
matcher = new AntPathRequestMatcher(url);
//与request对比,符合则说明权限表中有该请求的URL
if(matcher.matches(request)) {
return map.get(url);
}
}
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
4).MyUserDetialsService
package com.cxb.security.security;
import com.cxb.security.dao.UserDao;
import com.cxb.security.entity.Permission;
import com.cxb.security.entity.Role;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class MyUserDetialsService implements UserDetailsService {
@Autowired
UserDao userDao;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
//此处的user是entity包中的user
com.cxb.security.entity.User user = userDao.findByUsername(userName);
if (user != null) {
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
//获取用户的角色集合
List<Role> roles = user.getRoles();
//遍历角色集合,并获取每个角色拥有的权限
for (Role role : roles) {
List<Permission> permissions = role.getPermissions();
for (Permission permission :permissions) {
//为每个授权中心对象写入权限名
grantedAuthorities.add(new SimpleGrantedAuthority(permission.getName()));
}
}
/**此处的user是springsecurity中的一个实现了UserDetails接口的user类,因为我们没有将entity中的user去实现
* UserDetails接口,所以只能在此处调用实现好的构造方法
*/
return new User(user.getUsername(), user.getPassword(), grantedAuthorities);
}
return null;
}
}
5).WebSecurityConfig
package com.cxb.security.security;
import com.cxb.security.util.MD5Util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
MyUserDetialsService userService;
//配置加密
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(new PasswordEncoder() {
//加密
@Override
public String encode(CharSequence rawPassword) {
return MD5Util.encode((String) rawPassword);
}
//解密,前者是输入的密码,后者是数据库查询的密码
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(MD5Util.encode((String) rawPassword));
}
});
}
//配置拦截策略
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin().loginPage("/login")
.defaultSuccessUrl("/index")
.failureUrl("/login-error")
.permitAll()
.and().authorizeRequests().anyRequest().authenticated()
//不加上不验证。不知道为什么
.and().csrf().disable();
}
}
8.MD5Util工具类
package com.cxb.security.util;
import java.security.MessageDigest;
/**
* MD5加密工具
*
*/
public class MD5Util {
private static final String SALT = "tamboo";
public static String encode(String password) {
password = password + SALT;
MessageDigest md5 = null;
try {
md5 = MessageDigest.getInstance("MD5");
} catch (Exception e) {
throw new RuntimeException(e);
}
char[] charArray = password.toCharArray();
byte[] byteArray = new byte[charArray.length];
for (int i = 0; i < charArray.length; i++)
byteArray[i] = (byte) charArray[i];
byte[] md5Bytes = md5.digest(byteArray);
StringBuffer hexValue = new StringBuffer();
for (int i = 0; i < md5Bytes.length; i++) {
int val = ((int) md5Bytes[i]) & 0xff;
if (val < 16) {
hexValue.append("0");
}
hexValue.append(Integer.toHexString(val));
}
return hexValue.toString();
}
public static void main(String[] args) {
System.out.println(MD5Util.encode("admin"));
System.out.println(MD5Util.encode("user"));
}
}
9.UserDao
package com.cxb.security.dao;
import com.cxb.security.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserDao extends JpaRepository<User,Integer> {
User findByUsername(String userName);
}
10.PermissionDao
package com.cxb.security.dao;
import com.cxb.security.entity.Permission;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PermissionDao extends JpaRepository<Permission,Integer> {
}
11.LoginController
package com.cxb.security.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class LoginController {
@RequestMapping("/login")
public String login() { return "login"; }
@RequestMapping("/login-error")
public String error() {return "error"; }
@RequestMapping("/index")
public String index() { return "index";}
@RequestMapping("/admin")
public String admin() {return "admin"; }
@RequestMapping("/user")
public String user() {
return "user";
}
@RequestMapping("/index2")
public String index2() {
return "index2";
}
}
12.页面
1)admin.html
<!DOCTYPE html>
<html lang="en" >
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p class="bg-info">Admin权限访问</p>
</body>
</html>
2)error.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>error</h1>
</body>
</html>
3)index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>登录成功</h1>
<h2><a href="/logout">退出</a></h2>
</body>
</html>
4)index2.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
这是没录入数据库的url
</body>
</html>
5)login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form class="form-signin" action="/login" method="post">
<h2 class="form-signin-heading">用户登录</h2>
<table>
<tr>
<td>用户名:</td>
<td><input type="text" name="username" class="form-control" placeholder="请输入用户名"/></td>
</tr>
<tr>
<td>密码:</td>
<td><input type="password" name="password" class="form-control" placeholder="请输入密码" /></td>
</tr>
<tr>
<td colspan="2">
<button type="submit" class="btn btn-lg btn-primary btn-block" >登录</button>
</td>
</tr>
</table>
</form>
</body>
</html>
6)user.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
User权限访问
</body>
</html>
13.测试
1)http://localhost:8080/login 登陆admin账号
admin账号访问user页面是失败的,没有权限.
切换user账号测试