文章目录
springsecurity简介
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
Spring Security为基于J2EE企业应用软件提供了全面安全服务。 特别是使用领先的J2EE解决方案-spring框架开发的企业软件项目。
Spring Security安全包括两个主要操作, “认证”和“验证”(或权限控制)。 这就是Spring Security面向的两个主要方向。“认证” 是为用户建立一个他所声明的主体的过程, (“主体”一般是指用户,设备或可以在你系统中执行行动的其他系统)。 “验证”指的一个用户能否在你的应用中执行某个操作。 在到达授权判断之前,身份的主体已经由身份验证过程建立了。 这些概念是通用的,不是Spring Security特有的。
在身份验证层面,Spring Security广泛支持各种身份验证模式。 这些验证模型绝大多数都由第三方提供,或正在开发的有关标准机构提供的,例如Internet Engineering Task Force。 作为补充,Spring Security也提供了自己的一套验证功能。 Spring Security目前支持认证一体化和如下认证技术:
- HTTP BASIC authentication headers (一个基于IETF RFC的标准)
- HTTP Digest authentication headers (一个基于IETF RFC的标准)
- HTTP X.509 client certificate exchange (一个基于IETF RFC的标准)
- LDAP (一个非常常见的跨平台认证需要做法,特别是在大环境)
- Form-based authentication (提供简单用户接口的需求)
- OpenID authentication
- 基于预先建立的请求头进行认证 (比如Computer Associates Siteminder)
- JA-SIG Central Authentication Service (也被称为CAS,这是一个流行的开源单点登录系统)
- Transparent authentication context propagation for Remote Method Invocation (RMI) and HttpInvoker (一个Spring远程调用协议)
- Automatic “remember-me” authentication (这样你可以设置一段时间,避免在一段时间内还需要重新验证)
- Anonymous authentication (允许未认证的任何调用,自动假设一个特定的安全主体)
- Run-as authentication (这在一个会话内使用不同安全身份的时候是非常有用的)
- Java Authentication and Authorization Service (JAAS) 等
核心组件
以下通过系统化的讲解security中比较核心的几个关键对象
SecurityContextHolder
SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保存在SecurityContextHolder中。SecurityContextHolder默认使用ThreadLocal 策略来存储认证信息。看到ThreadLocal 也就意味着,这是一种与线程绑定的策略。Spring Security在用户登录时自动绑定认证信息到当前线程,在用户退出时,自动清除当前线程的认证信息。但这一切的前提,是你在web场景下使用Spring Security,而如果是Swing界面,Spring也提供了支持,SecurityContextHolder的策略则需要被替换,鉴于我的初衷是基于web来介绍Spring Security,所以这里以及后续,非web的相关的内容都一笔带过。
获取当前用户的信息
因为身份信息是与线程绑定的,所以可以在程序的任何地方使用静态方法获取用户信息。一个典型的获取当前登录用户的姓名的例子如下所示:
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
getAuthentication()返回了认证信息,再次getPrincipal()返回了身份信息,UserDetails便是Spring对身份信息封装的一个接口。Authentication和UserDetails的介绍在下面的小节具体讲解,本节重要的内容是介绍SecurityContextHolder这个容器。
Authentication
先看看这个接口的源码长什么样:
package org.springframework.security.core;// <1>
public interface Authentication extends Principal, Serializable { // <1>
Collection<? extends GrantedAuthority> getAuthorities(); // 获取当前用户的所有授权(角色子类SimpleGrantedAuthority)
Object getCredentials();// 通常是密码
Object getDetails();// 额外信息比如ip地址,证书等
Object getPrincipal();// 通常是用户名
boolean isAuthenticated();// 是否认证
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
spring Security是如何完成身份认证的?
-
用户名和密码被过滤器获取到,封装成Authentication,通常情况下是UsernamePasswordAuthenticationToken实现类。
-
AuthenticationManager 身份管理器负责验证这个Authentication
-
认证成功后,AuthenticationManager身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除)Authentication实例。
-
SecurityContextHolder安全上下文容器将第3步填充了信息的Authentication,通过SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。
这是一个抽象的认证流程,而整个过程中,如果不纠结于细节,其实只剩下一个AuthenticationManager 是我们没有接触过的了,这个身份管理器我们在后面的小节介绍。将上述的流程转换成代码,便是如下的流程:
public class AuthenticationExample {
private static AuthenticationManager am = new SampleAuthenticationManager();
public static void main(String[] args) throws Exception {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
while(true) {
System.out.println("Please enter your username:");
String name = in.readLine();
System.out.println("Please enter your password:");
String password = in.readLine();
try {
Authentication request = new UsernamePasswordAuthenticationToken(name, password);
Authentication result = am.authenticate(request);
SecurityContextHolder.getContext().setAuthentication(result);
break;
} catch(AuthenticationException e) {
System.out.println("Authentication failed: " + e.getMessage());
}
}
System.out.println("Successfully authenticated. Security context contains: " +
SecurityContextHolder.getContext().getAuthentication());
}
}
class SampleAuthenticationManager implements AuthenticationManager {
static final List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>();
static {
AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
}
public Authentication authenticate(Authentication auth) throws AuthenticationException {
if (auth.getName().equals(auth.getCredentials())) {
return new UsernamePasswordAuthenticationToken(auth.getName(),
auth.getCredentials(), AUTHORITIES);
}
throw new BadCredentialsException("Bad Credentials");
}
}
AuthenticationManager
Spring Security中的AuthenticationManager,ProviderManager ,AuthenticationProvider类名相似看,很容易搞得晕头转向,稍微梳理一下就可以理解清楚它们的联系和设计者的用意。
-
AuthenticationManager(接口)是认证相关的核心接口,也是发起认证的出发点,因为在实际需求中,我们可能会允许用户使用用户名+密码登录,同时允许用户使用邮箱+密码,手机号码+密码登录,甚至,可能允许用户使用指纹登录,所以说AuthenticationManager一般不直接认证,
-
ProviderManager: AuthenticationManager接口的常用实现类ProviderManager 内部会维护一个
List列表,存放多种认证方式,实际上这是委托者模式的应用(Delegate)。也就是说,核心的认证入口始终只有一个:AuthenticationManager,
不同的认证方式:用户名+密码(UsernamePasswordAuthenticationToken),邮箱+密码,手机号码+密码登录则对应了三个AuthenticationProvider。熟悉shiro的朋友可以把AuthenticationProvider理解成Realm。在默认策略下,只需要通过一个AuthenticationProvider的认证,即可被认为是登录成功。
只保留了关键认证部分的ProviderManager源码:
public class ProviderManager implements AuthenticationManager, MessageSourceAware,
InitializingBean {
// 维护一个AuthenticationProvider列表
private List<AuthenticationProvider> providers = Collections.emptyList();
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
// 依次认证
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
...
catch (AuthenticationException e) {
lastException = e;
}
}
// 如果有Authentication信息,则直接返回
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
//移除密码
((CredentialsContainer) result).eraseCredentials();
}
//发布登录成功事件
eventPublisher.publishAuthenticationSuccess(result);
return result;
}
...
//执行到此,说明没有认证成功,包装异常信息
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
prepareException(lastException, authentication);
throw lastException;
}
}
ProviderManager 中的List,会依照次序去认证,认证成功则立即返回,若认证失败则返回null,下一个AuthenticationProvider会继续尝试认证,如果所有认证器都无法认证成功,则ProviderManager 会抛出一个ProviderNotFoundException异常。
到这里,如果不纠结于AuthenticationProvider的实现细节以及安全相关的过滤器,认证相关的核心类其实都已经介绍完毕了:身份信息的存放容器SecurityContextHolder,身份信息的抽象Authentication,身份认证器AuthenticationManager及其认证流程。姑且在这里做一个分隔线。下面来介绍下AuthenticationProvider接口的具体实现。
DaoAuthenticationProvider
AuthenticationProvider最最最常用的一个实现便是DaoAuthenticationProvider。顾名思义,Dao正是数据访问层的缩写,也暗示了这个身份认证器的实现思路。由于本文是一个Overview,姑且只给出其UML类图:
按照我们最直观的思路,怎么去认证一个用户呢?用户前台提交了用户名和密码,而数据库中保存了用户名和密码,认证便是负责比对同一个用户名,提交的密码和保存的密码是否相同便是了。在Spring Security中。提交的用户名和密码,被封装成了UsernamePasswordAuthenticationToken,而根据用户名加载用户的任务则是交给了UserDetailsService,在DaoAuthenticationProvider中,对应的方法便是retrieveUser,虽然有两个参数,但是retrieveUser只有第一个参数起主要作用,返回一个UserDetails。还需要完成UsernamePasswordAuthenticationToken和UserDetails密码的比对,这便是交给additionalAuthenticationChecks方法完成的,如果这个void方法没有抛异常,则认为比对成功。比对密码的过程,用到了PasswordEncoder和SaltSource,密码加密和盐的概念相信不用我赘述了,它们为保障安全而设计,都是比较基础的概念。
UserDetails与UserDetailsService
UserDetails这个接口,它代表了最详细的用户信息,这个接口涵盖了一些必要的用户信息字段,具体的实现类对它进行了扩展。
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
它和Authentication接口很类似,比如它们都拥有username,authorities,区分他们也是本文的重点内容之一。Authentication的getCredentials()与UserDetails中的getPassword()需要被区分对待,前者是用户提交的密码凭证,后者是用户正确的密码,认证器其实就是对这两者的比对。Authentication中的getAuthorities()实际是由UserDetails的getAuthorities()传递而形成的。还记得Authentication接口中的getUserDetails()方法吗?其中的UserDetails用户详细信息便是经过了AuthenticationProvider之后被填充的。
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
UserDetailsService和AuthenticationProvider两者的职责常常被人们搞混,关于他们的问题在文档的FAQ和issues中屡见不鲜。记住一点即可,敲黑板!!!UserDetailsService只负责从特定的地方(通常是数据库)加载用户信息,仅此而已,记住这一点,可以避免走很多弯路。UserDetailsService常见的实现类有JdbcDaoImpl,InMemoryUserDetailsManager,前者从数据库加载用户,后者从内存中加载用户,也可以自己实现UserDetailsService,通常这更加灵活。
架构概览图
springsecurity中文文档参考:http://www.mossle.com/docs/springsecurity3/html/springsecurity.html
官网:https://docs.spring.io/spring-security/site/docs/4.2.11.RELEASE/reference/htmlsingle/#get-spring-security
springboot集成 springsecurity
关于springmvc继承springsecurity参考http://www.mossle.com/docs/springsecurity3/html/ns-config.html
springboot配置参考自基于官网java配置类方式配置spring security:https://docs.spring.io/spring-security/site/docs/4.2.11.RELEASE/reference/htmlsingle/#jc
环境:
- html使用thymeleaf
- springboot项目
helloworld程序
以下编写一个最简单的helloworld程序用于入门
application.yml配置
spring:
thymeleaf:
cache: false
mode: LEGACYHTML5
server:
port: 8888
maven依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.8.RELEASE</version>
</parent>
<dependencies>
<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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>nekohtml</groupId>
<artifactId>nekohtml</artifactId>
<version>1.9.6.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
登录成功页
添加一个控制层类
@Controller
public class TestController {
@RequestMapping("/toSuc")
public String suc() {
return "suc";
}
}
templates目录添加模板suc.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
this is success page
</body>
</html>
java配置
添加java配置类
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
/*
提供一个基于内存的用户名和密码存储 使用BCryptPasswordEncoder加密
*/
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("test").password(new BCryptPasswordEncoder().encode("123456")).roles("USER").build());
return manager;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/test").authenticated() //指定某些路径需要认证
.regexMatchers("/images/.+\\.jpg").permitAll() //images目录下的图片直接不需要认证
.anyRequest().authenticated() //除了上面的路径外都需要认证
.and()
.formLogin() //生成一个默认登陆页
.permitAll() //登陆页允许直接访问不授权
//注意 如果你访问 / 跳转到登录页面,当登录成功后会重新重定向到/ true可以设置 不跳转到之前这个页面 登录后直接到这个指定页面/toSuc
.defaultSuccessUrl("/toSuc",true)
.and()
.httpBasic().and().csrf().disable(); //禁止csrf
}
/**
指定使用上面的内存数据detailservice 使用bcrypt加密
**/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService()).passwordEncoder(new BCryptPasswordEncoder());
}
}
添加测试启用类
@SpringBootApplication
public class TestMain {
public static void main(String[] args) {
SpringApplication.run(TestMain.class);
}
}
效果测试
访问任意页面,比如 http://localhost:8888/test
自动跳转到内嵌的登陆页
输入java配置类中配置的用户名test和密码123456后,跳转到成功页
注意 java配置类的
.defaultSuccessUrl("/toSuc",true)
如果第二个参数false,登录成功自动跳转到之前 /test页面上
true可以设置 不跳转到之前这个页面 登录后直接设置的defaultSuccessUrl上
替换登陆页
控制层添加登录页路径映射
@RequestMapping("/rlogin")
public String rlogin() {
return "lg";
}
添加登陆页模板lg.html
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta charset="utf-8">
<style type="text/css">
fieldset{width: 350px;background:#4876FF;height: 200px;margin: 150px 400px 0px}
button{maogin: 0; padding: 0}
</style>
</head>
<body bgcolor="#7EC0EE">
<form action="/myauth" method="post">
<label>用户名:</label>
<input type="text" name="user" value="test">(请输入用户名)</br></br>
<label>密 码:</label>
<input type="password" name="pass" value="123456">(输入密码)<br><br>
<input type="submit" value="提交"><!--注意提交按钮一定不要有空的name 否则提交不带任何参数 shift-->
</form>
</body>
</html>
- java配置类修改默认登陆页/rlogin(默认是/login)
- 登录提交路径为/myauth(默认是/login)
- 登录用户名参数是user(默认是username)
- 登录密码参数是pass(默认是password)
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/test").authenticated()
.regexMatchers("/images/.+\\.jpg").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/rlogin") //登陆页路径
.loginProcessingUrl("/myauth") //表单提交的路径
.failureForwardUrl("/rlogin") //登录失败路径
.usernameParameter("user")
.passwordParameter("pass")
.permitAll() //注意所有和登录相关的都要放行,不能再antMatcher那里去放行
//注意 如果你访问 / 跳转到登录页面,当登录成功后会重新重定向到/ true可以设置 不跳转到之前这个页面 登录后直接到这个页面
.defaultSuccessUrl("/toSuc",true)
.and()
.httpBasic().and().csrf().disable();
}
重新访问测试(ok)
数据库验证
关于detailservice jdbc的实现是通过JdbcDaoImpl这个类 通过查看几个变量来梳理默认的权限模型
public class JdbcDaoImpl extends JdbcDaoSupport
implements UserDetailsService, MessageSourceAware {
//通过用户名查询用户信息的sql enabled表示用户是否启用
public static final String DEF_USERS_BY_USERNAME_QUERY = "select username,password,enabled "
+ "from users " + "where username = ?";
//通过用户名查询用户所有的权限比如 ROLE_ADMIN就是admin角色
public static final String DEF_AUTHORITIES_BY_USERNAME_QUERY = "select username,authority "
+ "from authorities " + "where username = ?";
//通过用户名查询组名和组权限,组就类似于角色的概念,一组用户
public static final String DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY = "select g.id, g.group_name, ga.authority "
+ "from groups g, group_members gm, group_authorities ga "
+ "where gm.username = ? " + "and g.id = ga.group_id "
+ "and g.id = gm.group_id";
mysql数据库添加表
#用户表
CREATE TABLE users(
username VARCHAR(50) NOT NULL PRIMARY KEY,
PASSWORD VARCHAR(200) NOT NULL,
enabled BOOLEAN NOT NULL);
#权限表
CREATE TABLE authorities (
username VARCHAR(50) NOT NULL,
authority VARCHAR(50) NOT NULL,
CONSTRAINT fk_authorities_users FOREIGN KEY(username) REFERENCES users(username));
CREATE UNIQUE INDEX ix_auth_username ON authorities (username,authority);
#分组表
CREATE TABLE groups (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
group_name VARCHAR(50) NOT NULL);
#分组权限
CREATE TABLE group_authorities (
group_id BIGINT NOT NULL,
authority VARCHAR(50) NOT NULL,
CONSTRAINT fk_group_authorities_group FOREIGN KEY(group_id) REFERENCES groups(id));
#用户分组关系表
CREATE TABLE group_members (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
group_id BIGINT NOT NULL,
CONSTRAINT fk_group_members_group FOREIGN KEY(group_id) REFERENCES groups(id));
插入一行数据
insert into `users` (`username`, `password`, `enabled`) values('test','$2a$10$AZ6YJq3D/s9H3vHrrW8.i.IltfvU4yfEj/EtBqNZhEls54q3JgCXe','1');
#注意一定要授权,不添加无法登陆成功
insert into `authorities` (`username`, `authority`) values('test','ROLE_ADMIN');
这里密码通过加密输出
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(8);//加盐长度
System.out.println(bCryptPasswordEncoder.encode("654321"));
修改替换之前内存配置的UserDetailService为
@Autowired DataSource dataSource;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
JdbcDaoImpl jdbcDao=new JdbcDaoImpl();
jdbcDao.setDataSource(dataSource);
auth.userDetailsService(jdbcDao).passwordEncoder(new BCryptPasswordEncoder());
}
或者直接设置jdbc
@Autowired PasswordEncoder passwordEncoder;
@Bean
public PasswordEncoder passwordEncoder(){
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(8);
return bCryptPasswordEncoder;
}
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication().dataSource(dataSource).passwordEncoder(passwordEncoder);
}
}
添加maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
添加数据源配置application.yml
spring:
datasource:
username: root
url: jdbc:mysql://localhost/test
driver-class-name: com.mysql.jdbc.Driver
password: 123456
最后测试登陆 成功
自定义验证
自定义验证要求实现一个UserDetailsService接口的实现类,返回UserDetails对象(包含查询出来的用户名密码和权限)
这里顺便实现一个MD5密码加密器,代码:
package io.github.jiaozi789;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
/**
* 使用md5算法
* @author 廖敏
* 创建日期 2019-03-13 9:41
**/
@Component
public class Md5PasswordEncoder implements PasswordEncoder {
/**
* 密码加密
* @param rawPassword 原始密码
* @return
*/
@Override
public String encode(CharSequence rawPassword) {
try {
MessageDigest messageDigest=MessageDigest.getInstance("MD5");
return Base64.getEncoder().encodeToString(messageDigest.digest(rawPassword.toString().getBytes()));
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}
/**
* 比较原始密码和加密后的密码是否相等
* @param rawPassword 原始密码,用户文本框输入的
* @param encodedPassword 加密后密码,数据库提取的
* @return
*/
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
try {
MessageDigest messageDigest=MessageDigest.getInstance("MD5");
String encodPass=Base64.getEncoder().encodeToString(messageDigest.digest(rawPassword.toString().getBytes()));
if(encodPass.equalsIgnoreCase(encodedPassword)){
return true;
}
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return false;
}
}
添加验证逻辑service
假设用户名是zs,密码是234567的md5加密base64字符串 role是ROLE_ADMIN
package io.github.jiaozi789;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
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;
/**
* @author 廖敏
* 创建日期 2019-03-13 9:39
**/
@Service
public class MyUserDetailService implements UserDetailsService {
private String userName="zs";
private String password="UI30yy9Nj4BRklYljPuXXw==";
private String role="ROLE_ADMIN";
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if(username.equalsIgnoreCase(userName)){
List<GrantedAuthority> authorityList=new ArrayList<>();
SimpleGrantedAuthority simpleGrantedAuthority=new SimpleGrantedAuthority(role);
authorityList.add(simpleGrantedAuthority);
User user=new User(userName,password,authorityList);
return user;
}
throw new UsernameNotFoundException(username);
}
}
修改配置类重新配置userDetailService
@Autowired
private MyUserDetailService myUserDetailService;
@Autowired
private Md5PasswordEncoder md5PasswordEncoder;
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailService).passwordEncoder(md5PasswordEncoder);
}
测试使用zs和234567登录是否成功
自定义登出
默认的登出路径为 /logout,在页面添加超链接登出,指定/logout,自动跳回到登陆页,清除cookie
可以通过配置设置logout页面
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/test").authenticated()
.regexMatchers("/images/.+\\.jpg").permitAll()
.anyRequest().authenticated()
.and()
.logout().logoutUrl("/mylogout")
}
然后定义路径映射mylogout,做一些处理
@RequestMapping(value = "/mylogout", method = RequestMethod.GET)
public String logout(Map model) {
model.put("msg", "你已经成功退出");
return "lg";
}
基于注解授权
Spring Security的声明式安全授权有两种方式,一种是以url模式匹配的方式,另一种是方法上使用注解声明权限
Spring Security默认是禁用注解的,要想开启注解,要在继承WebSecurityConfigurerAdapter的类加@EnableGlobalMethodSecurity注解,并在该类中将AuthenticationManager定义为Bean。
提供三种方法级基于注解权限控制
- 1.securedEnabled: Spring Security自带注解
- 2.jsr250Enabled: 基于简单角色控制的权限注解
- 3.prePostEnabled: 基于表达式
jsr250注解
启用方式:
@EnableGlobalMethodSecurity(jsr250Enabled = true)
支持以下注解
@DenyAll拒绝所有访问。
@RolesAllowed({"USER","ADMIN"})该方法只要具有“USER","ADMIN"任意一种权限就可以访问。这里可以省略前缀ROLE_,实际的权限可能是ROLE_ADMIN
@PermitAll允许所有访问
测试控制层添加方法
@DenyAll
@ResponseBody
@GetMapping("/test")
public String test() {
return "hello";
}
@RolesAllowed("ADMIN")
@ResponseBody
@GetMapping("/a")
public String a() {
return "hello";
}
登录后访问/test 用于是403拒绝
登录后访问/a 发现能访问 因为之前用户被赋予了ROLE_ADMIN权限如果改成ADMIN1就会显示403了
securedEnabled
启用方式
@EnableGlobalMethodSecurity(securedEnabled = true)
@Secured注解
在方法上指定安全性要求,只有对应角色的用户才可以调用这些方法,必须和用户定义的角色完全一样 比如ROLE_也要有
测试
@Secured("ROLE_ADMIN")
@ResponseBody
@GetMapping("/test")
public String test() {
return "hello";
}
@Secured("ROLE_ADMIN1")
@ResponseBody
@GetMapping("/a")
public String a() {
return "hello";
}
登录后 /test可以访问 /a无法访问
prePostEnabled注解
是一种基于表达式的注解,并可以自定义扩展,只需继承GlobalMethodSecurityConfiguration类就可以实现。当然,别忘了在该扩展类上添加注解:@EnableGlobalMethodSecurity(prePostEnabled=true)来启动这种注解支持。如果没有访问方法的权限,会抛出AccessDeniedException。
- @PreAuthorize
在方法执行之前执行,而且这里可以调用方法的参数,也可以得到参数值,这里利用JAVA8的参数名反射特性,如果没有JAVA8,那么也可以利用Spring Secuirty的@P标注参数,或利用Spring Data的@Param标注参数
比如service方法
@PreAuthorize("#userId == authentication.principal.userId or hasAuthority(‘ROLE_ADMIN’)")
Arcticle findArcticle(@P("userId") long userId ){ }
- @PostAuthorize
在方法执行之后执行,而且这里可以调用方法的返回值,如果EL为false,那么该方法也已经执行完了,可能会回滚。EL变量returnObject表示返回的对象。
比如:
@PostAuthorize("returnObject.userId == authentication.principal.userId or hasPermission(returnObject, 'ADMIN')")
User getUser();
- @PostFilter
在执行方法之后执行,而且这里可以调用方法的返回值,然后对返回值进行过滤或处理。EL变量returnObject表示返回的对象。只有方法返回的集合或数组类型的才可以使用。(与分页技术不兼容)
- @PreFilter
EL变量filterObject表示参数,如果有多个参数,可以使用@filterTarget注解参数,只有方法是集合或数组才行(与分页技术不兼容)。
目前支持的表达式
表达式 | 描述 |
---|---|
hasRole([role]) | 当前用户是否拥有指定角色。 |
hasAnyRole([role1,role2]) | 多个角色是一个以逗号进行分隔的字符串。如果当前用户拥有指定角色中的任意一个则返回true。 |
hasAuthority([auth]) | 等同于hasRole |
hasAnyAuthority([auth1,auth2]) | 等同于hasAnyRole |
Principle | 代表当前用户的principle对象 |
authentication | 直接从SecurityContext获取的当前Authentication对象 |
permitAll | 总是返回true,表示允许所有的 |
denyAll | 总是返回false,表示拒绝所有的 |
isAnonymous() | 当前用户是否是一个匿名用户 |
isRememberMe() | 表示当前用户是否是通过Remember-Me自动登录的 |
isAuthenticated() | 表示当前用户是否已经登录认证成功了。 |
isFullyAuthenticated() | 如果当前用户既不是一个匿名用户,同时又不是通过Remember-Me自动登录的,则返回true。 |
hasIpAddress | 判断用户访问的ip地址 比如 hasRole(‘USER’) and hasIpAddress(‘10.10.10.3’) |
hasPermission | 判断是某个UserDetail子类是否拥有某个权限,具体用法自行百度 |
比如用户的角色权限是ROLE_ADMIN 注意hasRole不需要添加前缀 hasRole(‘ADMIN’)
hasAuthority是用户拥有某项细粒度权限 比如可以设置为 USER_1_DELETE 使用表达式hasAuthority('USER_1_DELETE ')
如果是角色也必须全名匹配 hasAuthority('ROLE_ADMIN ')