目录
  1. 简介
  2. 工作流程
  3. 快速上手
    1. 建表
    2. 建表语句
    3. pom.xml
    4. application.yml
    5. User Bean
    6. Role Bean
    7. MyUserDetailsService
    8. MyInvocationSecurityMetadataSourceService
    9. MyAccessDecisionManager
    10. MyFilterSecurityInterceptor
    11. SecurityConfigurer
    12. MainController
    13. 页面
      1. login.html
      2. index.html
      3. admin.html
      4. common.html
      5. 401.html
Spring Boot Security MVC 模式的登录和权限控制

简介

  Spring Security,这是一种基于 Spring AOP 和 Servlet 过滤器的安全框架。它提供全面的安全性解决方案,同时在 Web 请求级和方法调用级处理身份确认和授权。

工作流程

从网上找了一张Spring Security 的工作流程图,如下。

图中标记的MyXXX,就是我们项目中需要配置的。

快速上手

建表

建表语句

DROP SCHEMA IF EXISTS security;
CREATE SCHEMA security;

DROP TABLE IF EXISTS security.system_user;
CREATE TABLE security.system_user(
id SERIAL,
username VARCHAR(50),
password VARCHAR(64),
register_datetime TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NOW(),
login_datetime TIMESTAMP(0) WITHOUT TIME ZONE,
logout_datetime TIMESTAMP(0) WITHOUT TIME ZONE,
PRIMARY KEY (id)
);
COMMENT ON TABLE security.system_user IS '系统用户表';
COMMENT ON COLUMN security.system_user.id IS '自增ID';
COMMENT ON COLUMN security.system_user.username IS '用户名';
COMMENT ON COLUMN security.system_user.password IS '密码';
COMMENT ON COLUMN security.system_user.register_datetime IS '注册时间';
COMMENT ON COLUMN security.system_user.login_datetime IS '登录时间';
COMMENT ON COLUMN security.system_user.logout_datetime IS '注销时间';

DROP TABLE IF EXISTS security.system_role;
CREATE TABLE security.system_role(
id SERIAL,
lable VARCHAR(100),
PRIMARY KEY (id)
);
COMMENT ON TABLE security.system_role IS '系统角色表';
COMMENT ON COLUMN security.system_role.id IS '系统自增ID';
COMMENT ON COLUMN security.system_role.lable IS '系统角色名称';

DROP TABLE IF EXISTS security.system_permission;
CREATE TABLE security.system_permission(
id SERIAL,
lable VARCHAR(100),
descripion TEXT,
url VARCHAR(255),
pid INTEGER,
PRIMARY KEY (id)
);
COMMENT ON TABLE security.system_permission IS '系统权限表';
COMMENT ON COLUMN security.system_permission.id IS '系统自增ID';
COMMENT ON COLUMN security.system_permission.lable IS '权限名称';
COMMENT ON COLUMN security.system_permission.descripion IS '权限说明';
COMMENT ON COLUMN security.system_permission.url IS '权限URL';
COMMENT ON COLUMN security.system_permission.pid IS '上级权限ID';

DROP TABLE IF EXISTS security.system_role_user;
CREATE TABLE security.system_role_user(
id SERIAL,
system_role_id INTEGER,
system_user_id INTEGER,
PRIMARY KEY (id)
);
COMMENT ON TABLE security.system_role_user IS '系统角色用户关系表';
COMMENT ON COLUMN security.system_role_user.id IS '系统自增ID';
COMMENT ON COLUMN security.system_role_user.system_role_id IS '系统角色ID';
COMMENT ON COLUMN security.system_role_user.system_user_id IS '系统用户ID';

DROP TABLE IF EXISTS security.system_role_permission;
CREATE TABLE security.system_role_permission(
id SERIAL,
system_role_id INTEGER,
system_permission_id INTEGER,
PRIMARY KEY (id)
);
COMMENT ON TABLE security.system_role_permission IS '系统角色权限关系表';
COMMENT ON COLUMN security.system_role_permission.id IS '系统自增ID';
COMMENT ON COLUMN security.system_role_permission.system_role_id IS '系统角色ID';
COMMENT ON COLUMN security.system_role_permission.system_permission_id IS '系统权限ID';

INSERT INTO security.system_user (id, username, password) VALUES (1,'user','e10adc3949ba59abbe56e057f20f883e');
INSERT INTO security.system_user (id, username , password) VALUES (2,'admin','e10adc3949ba59abbe56e057f20f883e');
INSERT INTO security.system_role (id, lable) VALUES (1,'USER');
INSERT INTO security.system_role (id, lable) VALUES (2,'ADMIN');
INSERT INTO security.system_permission (id, url, lable, pid) VALUES (1,'/user/common','common',0);
INSERT INTO security.system_permission (id, url, lable, pid) VALUES (2,'/user/admin','admin',0);
INSERT INTO security.system_role_user (system_user_id, system_role_id) VALUES (1, 1);
INSERT INTO security.system_role_user (system_user_id, system_role_id) VALUES (2, 1);
INSERT INTO security.system_role_user (system_user_id, system_role_id) VALUES (2, 2);
INSERT INTO security.system_role_permission (system_role_id, system_permission_id) VALUES (1, 1);
INSERT INTO security.system_role_permission (system_role_id, system_permission_id) VALUES (2, 1);
INSERT INTO security.system_role_permission (system_role_id, system_permission_id) VALUES (2, 2);

pom.xml

<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>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-security4</artifactId>
</dependency>

application.yml

spring:
thymeleaf:
mode: HTML5
encoding: UTF-8
cache: false

datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://localhost:5432/learning
username: learning
password: learning
db-name: learning
db-schema: security

User Bean

public class User implements UserDetails , Serializable {
private Long id;
private String username;
private String password;

private List<Role> authorities;

public Long getId() {
return id;
}

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

@Override
public String getUsername() {
return username;
}

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

@Override
public String getPassword() {
return password;
}

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

@Override
public List<Role> getAuthorities() {
return authorities;
}

public void setAuthorities(List<Role> authorities) {
this.authorities = authorities;
}

/**
* 用户账号是否过期
*/
@Override
public boolean isAccountNonExpired() {
return true;
}

/**
* 用户账号是否被锁定
*/
@Override
public boolean isAccountNonLocked() {
return true;
}

/**
* 用户密码是否过期
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}

/**
* 用户是否可用
*/
@Override
public boolean isEnabled() {
return true;
}
}

上面的 User 类实现了 UserDetails 接口,该接口是实现Spring Security 认证信息的核心接口。其中 getUsername 方法为 UserDetails 接口 的方法,这个方法返回 username,也可以是其他的用户信息,例如手机号、邮箱等。getAuthorities() 方法返回的是该用户设置的权限信息,在本实例中,从数据库取出用户的所有角色信息,权限信息也可以是用户的其他信息,不一定是角色信息。另外需要读取密码,最后几个方法一般情况下都返回 true,也可以根据自己的需求进行业务判断。

Role Bean

public class Role implements GrantedAuthority {
private Long id;
private String name;

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 getAuthority() {
return name;
}
}

Role 类实现了 GrantedAuthority 接口,并重写 getAuthority() 方法。权限点可以为任何字符串,不一定非要用角色名。

所有的Authentication实现类都保存了一个GrantedAuthority列表,其表示用户所具有的权限。GrantedAuthority是通过AuthenticationManager设置到Authentication对象中的,然后AccessDecisionManager将从Authentication中获取用户所具有的GrantedAuthority来鉴定用户是否具有访问对应资源的权限。

MyUserDetailsService

@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleMapper roleMapper;

@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
//查数据库
User user = userMapper.loadUserByUsername( userName );
if (null != user) {
List<Role> roles = roleMapper.getRolesByUserId( user.getId() );
user.setAuthorities( roles );
}

return user;
}
}

Service 层需要实现 UserDetailsService 接口,该接口是根据用户名获取该用户的所有信息, 包括用户信息和权限点

MyInvocationSecurityMetadataSourceService

@Component
public class MyInvocationSecurityMetadataSourceService implements FilterInvocationSecurityMetadataSource {
@Autowired
private PermissionMapper permissionMapper;

/**
* 每一个资源所需要的角色 Collection<ConfigAttribute>决策器会用到
*/
private static HashMap<String, Collection<ConfigAttribute>> map =null;


/**
* 返回请求的资源需要的角色
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
if (null == map) {
loadResourceDefine();
}
//object 中包含用户请求的request 信息
HttpServletRequest request = ((FilterInvocation) o).getHttpRequest();
for (Iterator<String> it = map.keySet().iterator() ; it.hasNext();) {
String url = it.next();
if (new AntPathRequestMatcher( url ).matches( request )) {
return map.get( url );
}
}

return null;
}

@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}

@Override
public boolean supports(Class<?> aClass) {
return true;
}

/**
* 初始化 所有资源 对应的角色
*/
public void loadResourceDefine() {
map = new HashMap<>(16);
//权限资源 和 角色对应的表 也就是 角色权限 中间表
List<RolePermisson> rolePermissons = permissionMapper.getRolePermissions();

//某个资源 可以被哪些角色访问
for (RolePermisson rolePermisson : rolePermissons) {

String url = rolePermisson.getUrl();
String roleName = rolePermisson.getRoleName();
ConfigAttribute role = new SecurityConfig(roleName);

if(map.containsKey(url)){
map.get(url).add(role);
}else{
List<ConfigAttribute> list = new ArrayList<>();
list.add( role );
map.put( url , list );
}
}
}
}

MyInvocationSecurityMetadataSourceService 类实现了 FilterInvocationSecurityMetadataSource,FilterInvocationSecurityMetadataSource 的作用是用来储存请求与权限的对应关系。

FilterInvocationSecurityMetadataSource接口有3个方法:

  • boolean supports(Class<?> clazz):指示该类是否能够为指定的方法调用或Web请求提供ConfigAttributes。
  • Collection getAllConfigAttributes():Spring容器启动时自动调用, 一般把所有请求与权限的对应关系也要在这个方法里初始化, 保存在一个属性变量里。
  • Collection getAttributes(Object object):当接收到一个http请求时, filterSecurityInterceptor会调用的方法. 参数object是一个包含url信息的HttpServletRequest实例. 这个方法要返回请求该url所需要的所有权限集合。

MyAccessDecisionManager

/**
* 决策器
*/
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {

private final static Logger logger = LoggerFactory.getLogger(MyAccessDecisionManager.class);

/**
* 通过传递的参数来决定用户是否有访问对应受保护对象的权限
*
* @param authentication 包含了当前的用户信息,包括拥有的权限。这里的权限来源就是前面登录时UserDetailsService中设置的authorities。
* @param object 就是FilterInvocation对象,可以得到request等web资源
* @param configAttributes configAttributes是本次访问需要的权限
*/
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
if (null == configAttributes || 0 >= configAttributes.size()) {
return;
} else {
String needRole;
for(Iterator<ConfigAttribute> iter = configAttributes.iterator(); iter.hasNext(); ) {
needRole = iter.next().getAttribute();

for(GrantedAuthority ga : authentication.getAuthorities()) {
if(needRole.trim().equals(ga.getAuthority().trim())) {
return;
}
}
}
throw new AccessDeniedException("当前访问没有权限");
}

}

/**
* 表示此AccessDecisionManager是否能够处理传递的ConfigAttribute呈现的授权请求
*/
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}

/**
* 表示当前AccessDecisionManager实现是否能够为指定的安全对象(方法调用或Web请求)提供访问控制决策
*/
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}

MyAccessDecisionManager 类实现了AccessDecisionManager接口,AccessDecisionManager是由AbstractSecurityInterceptor调用的,它负责鉴定用户是否有访问对应资源(方法或URL)的权限。

MyFilterSecurityInterceptor

@Component
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {


@Autowired
private FilterInvocationSecurityMetadataSource securityMetadataSource;

@Autowired
public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
super.setAccessDecisionManager(myAccessDecisionManager);
}


@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
invoke(fi);
}

public void invoke(FilterInvocation fi) throws IOException, ServletException {

InterceptorStatusToken token = super.beforeInvocation(fi);
try {
//执行下一个拦截器
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}

@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}

@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {

return this.securityMetadataSource;
}
}

每种受支持的安全对象类型(方法调用或Web请求)都有自己的拦截器类,它是AbstractSecurityInterceptor的子类,AbstractSecurityInterceptor 是一个实现了对受保护对象的访问进行拦截的抽象类。

AbstractSecurityInterceptor的机制可以分为几个步骤:

  1. 查找与当前请求关联的“配置属性(简单的理解就是权限)”
  2. 将 安全对象(方法调用或Web请求)、当前身份验证、配置属性 提交给决策器(AccessDecisionManager)
  3. 可选)更改调用所根据的身份验证
  4. 允许继续进行安全对象调用(假设授予了访问权)
  5. 在调用返回之后,如果配置了AfterInvocationManager。如果调用引发异常,则不会调用AfterInvocationManager。

AbstractSecurityInterceptor中的方法说明:

  • beforeInvocation()方法实现了对访问受保护对象的权限校验,内部用到了AccessDecisionManager和AuthenticationManager;
  • finallyInvocation()方法用于实现受保护对象请求完毕后的一些清理工作,主要是如果在beforeInvocation()中改变了SecurityContext,则在finallyInvocation()中需要将其恢复为原来的SecurityContext,该方法的调用应当包含在子类请求受保护资源时的finally语句块中。
  • afterInvocation()方法实现了对返回结果的处理,在注入了AfterInvocationManager的情况下默认会调用其decide()方法。

了解了AbstractSecurityInterceptor,就应该明白了,我们自定义MyFilterSecurityInterceptor就是想使用我们之前自定义的 AccessDecisionManager 和 securityMetadataSource。

SecurityConfigurer

@Configuration
@EnableWebSecurity
public class SecurityConfigurer extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserDetailsService userService;


@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {

//校验用户
auth.userDetailsService( userService ).passwordEncoder( new PasswordEncoder() {
//对密码进行加密
@Override
public String encode(CharSequence charSequence) {
System.out.println(charSequence.toString());
return DigestUtils.md5DigestAsHex(charSequence.toString().getBytes());
}
//对密码进行判断匹配
@Override
public boolean matches(CharSequence charSequence, String s) {
String encode = DigestUtils.md5DigestAsHex(charSequence.toString().getBytes());
boolean res = s.equals( encode );
return res;
}
} );

}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/","index","/login","/login-error","/401","/css/**","/js/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage( "/login" ).failureUrl( "/login-error" )
.and()
.exceptionHandling().accessDeniedPage( "/401" );
http.logout().logoutSuccessUrl( "/" );
}
}

@EnableWebSecurity注解以及WebSecurityConfigurerAdapter一起配合提供基于web的security。自定义类 继承了WebSecurityConfigurerAdapter来重写了一些方法来指定一些特定的Web安全设置。

MainController

@Controller
public class MainController {
@RequestMapping("/")
public String root() {
return "redirect:/index";
}

@RequestMapping("/index")
public String index() {
return "index";
}

@RequestMapping("/login")
public String login() {
return "login";
}

@RequestMapping("/login-error")
public String loginError(Model model) {
model.addAttribute( "loginError" , true);
return "login";
}

@GetMapping("/401")
public String accessDenied() {
return "401";
}

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

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

页面

login.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<h1>Login page</h1>
<p th:if="${loginError}" class="error">用户名或密码错误</p>
<form th:action="@{/login}" method="post">
<label for="username">用户名</label>:
<input type="text" id="username" name="username" autofocus="autofocus" />
<br/>
<label for="password">密 码</label>:
<input type="password" id="password" name="password" />
<br/>
<input type="submit" value="登录" />
</form>
<p><a href="/index" th:href="@{/index}"></a></p>
</body>
</html>

index.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<h2>page list</h2>
<a href="/user/common">common page</a>
<br/>
<a href="/user/admin">admin page</a>
<br/>
<form th:action="@{/logout}" method="post">
<input type="submit" class="btn btn-primary" value="注销"/>
</form>
</body>
</html>

admin.html

<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<title>admin page</title>
</head>
<body>
success admin page!!!
</body>
</html>

common.html

<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<title>common page</title>
</head>
<body>
success common page!!!
</body>
</html>

401.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>401 page</title>
</head>
<body>
<div>
<div>
<h2>权限不够</h2>
<p>拒绝访问!</p>
</div>
</div>
</body>
</html>

  最后运行项目,可以分别用 user、admin 账号 去测试认证和授权是否正确。

Github项目地址:https://github.com/gf-huanchupk/SpringBootLearning/tree/master/springboot-security

文章作者: Gadfly
文章链接: https://blog.gadfly.pub/2020/03/17/cheng-xu-she-ji/spring-boot-security-mvc-mo-shi-de-deng-lu-he-quan-xian-kong-zhi/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 牛虻的世界
打赏
  • 微信
  • 支付寶

评论