文章目录
安全控制Spring Security
概念
什么是spring security?
Spring Security是为基于Spring的应用程序提供声明式安全保护的安全性框架。Spring Security提供了完整的安全性解决方案,它能够在web请求级别和方法调用级别处理身份认证和授权。充分利用了依赖注入(DI)和面向切面(AOP)的技术。
安全框架有两个重要的概念:认证(Authentication)和授权(Authorization)。认证即确认用户可以访问当前系统;授权即确定用户在当前系统下所拥有的功能权限。
1.1 简单示例
通过springboot项目添加Spring Security依赖,测试安全控制
<!--security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
通过templates实现一个简单的启动成功提示页面
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>启动成功</title>
</head>
<body>
<div>
<h1>启动成功</h1>
</div>
</body>
</html>
通过页面接口访问,配置context-path和端口
server.servlet.context-path=/MySpringBootDemo
server.port=8899
启动项目,在启动日志中有密码
页面访问接口
用户名:user
密码:启动日志中的
登录成功以后就会进入到提示页面
当然实际项目中不会这样使用的,最简单的方式在配置文件中配置用户名密码,如下:
spring.security.user.name=user
spring.security.user.password=123
即使如此实际项目一般不会这么用,因为直接这样使用spring security会拦截所有访问请求,必须登录才能调用。
- 项目中可能某些接口或者页面是可以直接访问的,不需要拦截,那么就需要放行,需要定制拦截;
- 项目中有些页面是通过权限控制的,可能只有管理员登录才能访问,普通用户访问不了,那么就要做权限管理,需要制定角色等;
- 项目一般会有自己定制化的登录页面,默认的登录页面太简单了。
当然安全控制的场景很多,项目会有不同的定制操作,不会直接依赖默认的Spring Security
1.2 Spring Security的模块
Spring Security3.2分为11个模块
ACL: 支持通过访问控制列表(access control list ACL)为域提供安全性;
切面(Aspect): 一个很小的模块,当时用Spring Security注解时,会使用基于AspectJ的切面,而不是使用标准的Spring AOP;
CAS客户端: 提供与Jasig的中心认证服务(Central Authentication Service CAS)进行集成的功能;
配置(Configuration): 包含通过XML和Java配置Spring Security的功能支持;
核心(core): 提供Spring Security基本库;
加密(Cryptography): 提供了加密和密码编码功能;
LDAP: 支持基于LDAP进行认证;
OpenID: 支持使用OpenID进行集中式认证;
Remoting: 提供了对Spring Remoting的支持;
标签库(Tag Library): Spring Security的JSP标签库;
Web: 提供了Spring Security基于Filter的Web安全性支持;
1.3 Spring Security的配置
开启Spring Security配置很简单,自定义一个配置类,此配置类继承WebSecurityConfigurerAdapter类,通过重写configure方法来配置相关的安全配置,通过注解@EnableWebSecurity开启安全性功能,若应用程序使用SpringMVC,那么可以使用@EnableWebMvcSecurity,次注解配置了一个Spring MVC参数解析器,这样的话处理器方法能够通过带有@AuthenticationPrincipal注解的参数获得认证用户的principal(或username)。例如:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//通过重载,配置user-detail服务
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
}
//通过重载,配置如何通过拦截器保护请求
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
//通过重载,配置Spring Security的Filter链
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
}
}
1.4 用户认证
认证需要我们有一套用户数据的来源,而授权则是对于某个用户有相应的角色权限。在Spring Security里我们通过重写以下方法实现定制化。
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
}
内存用户
AUthenticationManagerBuilder有多个方法可以用来配置Spring Security对认证的支持。通过inMemoryAuthentication()方法,我们可以启用,配置并任意填充基于内存的用户存储。
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user").password("123").roles("ROLE_USER")
.and()
.withUser("admin").password("456").authorities("USER","ADMIN");
}
}
上述示例SecurityConfig重写了configure()方法,填充了两个用户,通过查看源码roles()调用了一下方法,会判断角色的命名,最后返回一个UserBuilder,而UserBuilder是org.springframework.security.core.userdetails.User的内部类,User实现了UserDetails
public UserBuilder roles(String... roles) {
List<GrantedAuthority> authorities = new ArrayList<>(
roles.length);
for (String role : roles) {
Assert.isTrue(!role.startsWith("ROLE_"), () -> role
+ " cannot start with ROLE_ (it is automatically added)");
authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
}
return authorities(authorities);
}
同样authorites可以查看源码
public UserBuilder authorities(String... authorities) {
return authorities(AuthorityUtils.createAuthorityList(authorities));
}
配置方法如下:
accountExpired(boolean): 定义账号是否已经过期
accountLocked(boolean): 定义账号是否已经锁定
and(): 用来连接配置
authorites(GrantedAuthority…): 授予某个用户一项或者多项权限
authorites(List<? extends GrantedAuthority>): 授予某个用户一项或者多权限
credentialsExpired(boolean): 定义凭证是否已经过期
disabled(boolean): 定义账号是否已经被禁用
password(String): 定义用户密码
roles(String…): 授予某个用户一项或多项角色
数据库用户
用户信息并非总是存在内存中,一般情况下用户信息存在数据库中,那么怎么给数据库中的用户认证授权呢?
可以通过jdbcAuthentication()方法
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication()
.dataSource(dataSource);
}
默认的Spring Security内部实现了SQL查询语句,类org.springframework.security.provisioning.JdbcUserDetailsManager中定义了SQL查询语句,如下:
public static final String DEF_USER_EXISTS_SQL = "select username from users where username = ?";
public static final String DEF_GROUP_AUTHORITIES_QUERY_SQL = "select g.id, g.group_name, ga.authority "
+ "from groups g, group_authorities ga "
+ "where g.group_name = ? "
+ "and g.id = ga.group_id ";
但是我们项目中的数据库用户表不一定是按照默认SQL设计的,那么就需要用到定制化的SQL语句,如下:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery("select user, password, true from t_user where username = ?")
.authoritiesByUsernameQuery("select username, 'ROLE_USER' from t_user where username = ?");
}
自定义sql语句,重写认证和基本权限查询语句,所有查询都将用户名作为唯一参数。
一般我们用户的密码是需要加密处理的,那么需要借助passwordEncoder()方法指定一个密码转码器。例如:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery("select user, password, true from t_user where username = ?")
.authoritiesByUsernameQuery("select username, 'ROLE_USER' from t_user where username = ?")
.passwordEncoder(new PasswordEncoder() {
@Override
public String encode(CharSequence rawPassword) {
return DigestUtils.md5Hex(rawPassword.toString());
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return StringUtils.equals(DigestUtils.md2Hex(rawPassword.toString()),encodedPassword);
}
});
}
以上通过内部类方式实现了对密码的MD5加密。
通用用户
通过自定义实现UserDetailsService接口,指定用户名,密码以及角色,例如:
@Service
public class CustomUserServiceImpl implements UserDetailsService{
@Resource
private UserService userService;
@Resource
private UserMapper userMapper;
@Resource
private UserRoleMapper userRoleMapper;
@Resource
private RoleMapper roleMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//获取一个用户
UserBean user =userMapper.findUserByUserName(username);
List<GrantedAuthority> authorities = new ArrayList<>();
//设置角色为ROLE_ADMIN
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN");
return new User(user.getUsername(), user.getPassword(), authorities);
}
}
通过表单提交获取username,比如通过template自定义个一登录页面,提交通过表达提交登录信息,此方法就可以获取到登录名,通过登录名称查询用户信息,当然如果自定义登录页面通过手机号码登录,那么此username即为手机号码。自定义登录也后面会有示例。
同时还需要在配置类中注册这个自定义类
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
UserDetailsService customUserService() {
return new CustomUserServiceImpl();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserService());
}
.......
}
自定义登录页面表达例如:
<form th:action="@{/login}" method="post" class="am-form" >
<div class="loginInput am-input-group am-u-lg-10 col-md-offset-2 am-form-icon am-form-feedback" style="margin-left: 0">
<div class="am-input-group-label"><i class="fa fa-phone fa-lg"></i></div>
<input type="text" name="username" class="am-form-field" id="phone" placeholder="请输入手机号"
onblur="login.checkPhone()" onfocus="login.focusPhone()">
<span id="phone_null" class="icons am-icon-warning" style="color: orange"></span>
<span id="phone_error" class="icons am-icon-times" style="color: red"></span>
</div>
<br>
<div class="loginInput am-input-group am-u-lg-10 col-md-offset-2 am-form-icon am-form-feedback" style="margin-left: 0">
<div class="am-input-group-label"><i class="fa fa-lock fa-lg"></i></div>
<input type="password" name="password" id="password" placeholder="请输入密码"
onfocus="login.focusPassword()">
<span id="password_null" class="icons am-icon-warning" style="color: orange"></span>
</div>
<br/>
<div class="am-u-lg-10 col-md-offset-2">
<label class="rememberMe" for="remember-me">
<input id="remember-me" type="checkbox">
记住密码
</label>
</div>
<br>
<div class="am-input-group am-u-lg-10 col-md-offset-2" style="margin-left: 0">
<input type="submit" id="loginFormSubmit" value="登 录" class="am-btn am-btn-primary am-btn-sm am-fl"
style="border-radius: 5px" onclick="login.submit()">
<button type="button" class="am-btn am-btn-default am-btn-sm am-fr " data-am-modal="{target: '#my-alert'}" style="border-radius: 5px">
忘记密码 ^_^?
</button>
</div>
<div class="login_error" th:if="${param.error}">
<span class="am-alert am-alert-secondary" data-am-alert style="margin-left: 15%;background-color: #d85a37;color: white;border-radius: 5px">用户名不存在或密码错误!</span>
</div>
</form>
1.5 拦截请求
项目中会有很多接口,那么哪些接口哪些接口不需要认证即可访问,哪些需要通过授权认证才能访问,那么安全控制的方式实现拦截,Spring Security可以通过重写以下方法实现:
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
}
Spring Security使用以下匹配器来匹配请求路径:
- antMatchers: 使用Ant风格的路径匹配
- regexMatchers: 使用正则表达式匹配路径
- anyRequest: 匹配所有请求路径
安全处理方法:
方法 | 用途 |
---|---|
access(string) | Spring EL表达式结果为true时可以访问 |
anonymous() | 匿名可访问 |
permitAll() | 用户可任意访问 |
authenticated | 登录以后可访问 |
denyAll | 用户不能访问 |
hasAnyAuthority(String…) | 如果用户有参数,则其中任一权限可访问 |
hasAnyRole(String…) | 如果用户有参数,则其中任一角色可访问 |
hasAuthority(String) | 如果用户有参数,则其权限可访问 |
hasIpAddress(String) | 如果用户来自参数中的IP则可以访问 |
hasRole(String) | 若用户有参数中的角色可访问 |
rememberMe() | 允许通过remember-me登录访问 |
fullyAuthenticated() | 用户完全认证可访问(非remember me下自动登录) |
具体使用方式可以根据源码,例如:hasAnyRole(String…)
通过查看源码发现角色都是“ROLE_”开头
private static String hasAnyRole(String... authorities) {
String anyAuthorities = StringUtils.arrayToDelimitedString(authorities,
"','ROLE_");
return "hasAnyRole('ROLE_" + anyAuthorities + "')";
}
与hasAnyAuthority(String…)的不同,如下:
private static String hasAnyAuthority(String... authorities) {
String anyAuthorities = StringUtils.arrayToDelimitedString(authorities, "','");
return "hasAnyAuthority('" + anyAuthorities + "')";
}
也就是“ROLE_”开头的认为是角色,否则认为是权限。
例如:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/", "/index")
.permitAll()
.antMatchers("/editor", "/user").hasAnyRole("USER")
.antMatchers("/ali", "/mylove").hasAnyAuthority()
.antMatchers(HttpMethod.POST, "/superadmin").hasAnyRole("SUPERADMIN")
.and()
.formLogin().loginPage("/login").failureUrl("/login?error").defaultSuccessUrl("/user")
.and()
.headers().frameOptions().sameOrigin()
.and()
.logout().logoutUrl("/logout").logoutSuccessUrl("/")
.and()
.rememberMe()
.tokenValiditySeconds(2419200)
.key("blogKey");
http.csrf().disable();
}
antMatchers()有两个重载的方法,分别是:
public C antMatchers(HttpMethod method, String... antPatterns)
public C antMatchers(String... antPatterns)
方法1可以指定请求接口的类型;方法二不需要指定,只通过接口地址拦截。
1.6 定制登录
定制化登录也可以重写以下方法
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
}
例如:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.loginPage("/login") //登录页面接口
.failureUrl("/login?error") //登录失败页面接口
.defaultSuccessUrl("/") //默认登录成功以后跳转页面接口地址
.and() //连接不同配置指令
.rememberMe() //开启cookie保存用户信息
.tokenValiditySeconds(1209600) //token的过期时间
.key("myKey") //cookie的私钥
.and()
.logout() //退出登录
.logoutUrl("/logout") //退出登录接口地址
.logoutSuccessUrl("/"); //退出成功跳转页面
http.csrf().disable(); //禁用CSRF防护功能,可以跨域访问
}
formLogin() 定制登录操作,查看源码发现调用了FormLoginConfigurer类
public FormLoginConfigurer() {
super(new UsernamePasswordAuthenticationFilter(), null);
usernameParameter("username");
passwordParameter("password");
}
获取参数username,password即用户名和密码,再跟进UsernamePasswordAuthenticationFilter
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
public AntPathRequestMatcher(String pattern, String httpMethod) {
this(pattern, httpMethod, true);
}
loginPage(): 定制登录页面地址
failureUrl() : 定制登录失败地址
rememberMe() : 开启cookie存储用户信息
tokenValiditySeconds(): 指定 cookie有效期
key(): 指定cookie中私钥
logout(): 注销行为
logoutUrl(): 注销的Url地址
logoutSuccessUrl(): 注销成功后转向的页面
1.7 保护视图
SpringBoot中使用template自定义页面的时候,可能有些功能展示需要根据不同角色,Spring Security也提供了一些标签:
sec:authentication —— 渲染认证对象的属性
sec:authorize —— 基于表达式的计算结果,条件性的渲染内容
sec:authorize-acl —— 基于表达式的计算结果,条件性的渲染内容
sec:authorize-expr —— sec:authorize属性的别名
sec:authorize-url —— 基于给定URL路径相关的安全规则,条件性的渲染内容
当然是用这些标签的话与需要在html中添加依赖,例如:
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4"
比如登录成功以后需要显示用户名以及点击按钮,例如:
<button class="personalSpace am-btn am-btn-secondary am-topbar-btn am-btn-sm am-dropdown-toggle"
data-am-dropdown-toggle>Hello<span sec:authentication="name"></span> <span
class="am-icon-caret-down"></span></button>
未登录
登录成功
当然登录成功以后有些菜单可以通过不同角色判断是否显示,例如:
<ul class="am-dropdown-content">
<li><a href="/user" sec:authorize="hasRole('ROLE_USER')">个人主页</a></li>
<li><a href="/superadmin" sec:authorize="hasRole('ROLE_SUPERADMIN')">后台管理</a></li>
<li><a href="/user" class="news">消息<span
class="newsNum am-badge am-badge-danger"></span></a></li>
<li><a class="feedbackClick">反馈</a></li>
<li><a th:href="@{/logout}">退出登录</a></li>
</ul>
使用sec:authorize 标签判断不同角色,以上示例中“个人主页”角色为ROLE_USER的可以查看,而后台管理需要ROLE_SUPERADMIN角色才能查看,而反馈是没有限定的。
以上即为Spring Security的一个简单入门学习。