学习目标:
Spring Security 应用级别的安全主要包含两个主要部分,即登录认证(Authentication)和访问授权(Authorization),首先用户登录的时候传入登录信息,登录验证器完成登录认证并将登录认证好的信息存储到请求上下文,然后再进行其他操作,如在进行接口访问、方法调用时,权限认证器从上下文中获取登录认证信息,然后根据认证信息获取权限信息,通过权限信息和特定的授权策略决定是否授权。
pring security 的核心功能主要包括:
认证 (你是谁)
授权 (你能干什么)
攻击防护 (防止伪造身份)
其核心就是一组过滤器链,项目启动后将会自动配置。最核心的就是 Basic Authentication Filter 用来认证用户的身份,一个在spring security中一种过滤器处理一种认证方式
学习内容:
接下来,我们就通过一个具体的案例,来讲解如何进行Spring Security的整合,然后借助Spring Security实现登录认证和访问控制。
生成项目模板为方便我们初始化项目,Spring Boot给我们提供一个项目模板生成网站。
1. 打开浏览器,访问:https://start.spring.io/
2. 根据页面提示,选择构建工具,开发语言,项目信息等。
3. 点击 Generate the project,生成项目模板,生成之后会将压缩包下载到本地。
4. 使用IDE导入项目,我这里使用Eclipse,通过导入Maven项目的方式导入。
添加相关依赖清理掉不需要的测试类及测试依赖,添加 Maven 相关依赖,这里需要添加上web、swagger、spring security、jwt和fastjson的依赖,Swagge和fastjson的添加是为了方便接口测试。
pom.xml
添加相关配置4.0.0 org.springframework.boot spring-boot-starter-parent2.1.5.RELEASE com.louis.springboot demo0.0.1-SNAPSHOT demo Demo project for Spring Boot 1.8 org.springframework.boot spring-boot-starter-webio.springfox springfox-swagger22.9.2 io.springfox springfox-swagger-ui2.9.2 org.springframework.boot spring-boot-starter-securityio.jsonwebtoken jjwt0.9.1 com.alibaba fastjson1.2.58 org.springframework.boot spring-boot-maven-pluginsrc/main/java **/sqlmap*.* true
1.添加swagger 配置
添加一个swagger 配置类,在工程下新建 config 包并添加一个 SwaggerConfig 配置类,除了常规配置外,加了一个令牌属性,可以在接口调用的时候传递令牌。令牌使用JWT token
SwaggerConfig.java
package com.rkhd.ienterprise.apps.bi.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.service.Parameter;
import springfox.documentation.spi.documentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.ArrayList;
import java.util.List;
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Bean
public Docket createRestApi(){
// 添加请求参数,我们这里把token作为请求头部参数传入后端
ParameterBuilder parameterBuilder = new ParameterBuilder();
List parameters = new ArrayList();
parameterBuilder.name("Authorization").description("令牌").modelRef(new ModelRef("string")).parameterType("header")
.required(false).build();
parameters.add(parameterBuilder.build());
return new Docket(documentationType.SWAGGER_2).apiInfo(apiInfo()).select().apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any()).build().globalOperationParameters(parameters);
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
//页面标题
.title("BI数据智能API接口文档")
//创建人
.contact(new Contact("雷鹏程", "https://wiki.ingageapp.com/pages/viewpage.action?pageId=64266431","leipc@xiaoshouyi.com"))
//版本号
.version("1.0")
//描述
.description("BI系统API描述")
.build();
}
}
swagger界面如下:
2.添加跨域 配置
package com.rkhd.ienterprise.apps.bi.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 允许跨域访问的路径
registry.addMapping("
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
// 使用自定义登录身份认证组件
auth.authenticationProvider(new JwtAuthenticationProvider(userDetailsService));
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 禁用 csrf, 由于使用的是JWT,我们这里不需要csrf
http.cors().and().csrf().disable()
.authorizeRequests()
// 跨域预检请求
.antMatchers(HttpMethod.OPTIONS, "**").permitAll()
.antMatchers("/webjars
@Configuration
public class CaptchaConfig {
@Bean
public DefaultKaptcha defaultKaptcha(){
//验证码生成器
DefaultKaptcha defaultKaptcha=new DefaultKaptcha();
//配置
Properties properties = new Properties();
//是否有边框
properties.setProperty("kaptcha.border", "yes");
//设置边框颜色
properties.setProperty("kaptcha.border.color", "105,179,90");
//边框粗细度,默认为1
properties.setProperty("kaptcha.border.thickness","1");
//验证码
properties.setProperty("kaptcha.session.key","code");
//验证码文本字符颜色 默认为黑色
properties.setProperty("kaptcha.textproducer.font.color", "blue");
//设置字体样式
properties.setProperty("kaptcha.textproducer.font.names", "宋体,楷体,微软雅黑");
//字体大小,默认40
properties.setProperty("kaptcha.textproducer.font.size", "30");
//字符长度,默认为5
properties.setProperty("kaptcha.textproducer.char.length", "4");
//字符间距 默认为2
properties.setProperty("kaptcha.textproducer.char.space", "4");
//验证码图片宽度 默认为200
properties.setProperty("kaptcha.image.width", "100");
//验证码图片高度 默认为40
properties.setProperty("kaptcha.image.height", "40");
Config config = new Config(properties);
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
package com.rkhd.ienterprise.apps.bi.controller;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.IOException;
@RestController
public class CaptchaController {
@Autowired
private DefaultKaptcha defaultKaptcha;
@ApiOperation(value = "验证码")
@GetMapping(value = "/captcha",produces = "image/jpeg")
public void captcha(HttpServletRequest request, HttpServletResponse response){
// 定义response输出类型为image/jpeg类型
response.setDateHeader("Expires", 0);
// Set standard HTTP/1.1 no-cache headers.
response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
// Set IE extended HTTP/1.1 no-cache headers (use addHeader).
response.addHeader("Cache-Control", "post-check=0, pre-check=0");
// Set standard HTTP/1.0 no-cache header.
response.setHeader("Pragma", "no-cache");
// return a jpeg
response.setContentType("image/jpeg");
//-------------------生成验证码 begin --------------------------
//获取验证码文本内容
String text = defaultKaptcha.createText();
System.out.println("验证码内容:"+text);
//将验证码文本内容放入session
request.getSession().setAttribute("captcha",text);
//根据文本验证码内容创建图形验证码
BufferedImage image = defaultKaptcha.createImage(text);
ServletOutputStream outputStream = null;
try {
outputStream = response.getOutputStream();
//输出流输出图片,格式为jpg
ImageIO.write(image,"jpg",outputStream);
outputStream.flush();
} catch (IOException e) {
e.printStackTrace();
}finally {
if (null!=outputStream){
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//-------------------生成验证码 end --------------------------
}
}
登录身份认证组件
上面说到登录认证是通过调用 AuthenticationManager 的 authenticate(token) 方法实现的,而 AuthenticationManager 又是通过调用 AuthenticationProvider 的 authenticate(Authentication authentication) 来完成认证的,所以通过定制 AuthenticationProvider 也可以完成各种自定义的需求,我们这里只是简单的继承 DaoAuthenticationProvider 展示如何自定义,具体的大家可以根据各自的需求按需定制。
package com.rkhd.ienterprise.apps.bi.security;
import com.rkhd.ienterprise.security.MD5Util;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
public class JwtAuthenticationProvider extends DaoAuthenticationProvider {
public JwtAuthenticationProvider(UserDetailsService userDetailsService) {
setUserDetailsService(userDetailsService);
setPasswordEncoder(new BCryptPasswordEncoder());
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 可以在此处覆写整个登录认证逻辑
return super.authenticate(authentication);
}
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
String password = null;
// 可以在此处覆写密码验证逻辑
if (authentication.getCredentials() == null) {
this.logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
} else {
String presentedPassword = authentication.getCredentials().toString();
try {
password = MD5Util.encrypt(presentedPassword);
} catch (Exception e) {
e.printStackTrace();
}
if (!password.equals(userDetails.getPassword())) {
this.logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
// super.additionalAuthenticationChecks(userDetails, authentication);
}
}
登录认证检查过滤器
package com.rkhd.ienterprise.apps.bi.security;
import com.rkhd.ienterprise.apps.bi.utils.SecurityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
@Autowired
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
// 获取token, 并检查登录状态
SecurityUtils.checkAuthentication(request);
chain.doFilter(request, response);
}
}
自定义令牌对象:
package com.rkhd.ienterprise.apps.bi.security;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
public class JwtAuthenticatioToken extends UsernamePasswordAuthenticationToken {
private static final long serialVersionUID = 1L;
private String token;
public JwtAuthenticatioToken(Object principal, Object credentials){
super(principal, credentials);
}
public JwtAuthenticatioToken(Object principal, Object credentials, String token){
super(principal, credentials);
this.token = token;
}
public JwtAuthenticatioToken(Object principal, Object credentials, Collection extends GrantedAuthority> authorities, String token) {
super(principal, credentials, authorities);
this.token = token;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public static long getSerialversionuid() {
return serialVersionUID;
}
}
用户登录认证信息查询
package com.rkhd.ienterprise.apps.bi.security;
import com.rkhd.ienterprise.apps.bi.dto.BiUser;
import com.rkhd.ienterprise.apps.bi.exception.ManageException;
import com.rkhd.ienterprise.apps.bi.service.impl.BoardServiceImpl;
import com.rkhd.ienterprise.apps.bi.service.user.UserService;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
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.List;
import java.util.Set;
import java.util.stream.Collectors;
import static com.rkhd.ienterprise.apps.bi.utils.BiResultEnum.BOARD_SAVE_DB_ERROR_2;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private static final Logger LOG = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
BiUser user = null;
Set roles = null;
try {
user = userService.getUsers(username);
if (user == null) {
throw new UsernameNotFoundException("该用户不存在");
}
} catch (ManageException e) {
LOG.error(ExceptionUtils.getStackTrace(e));
}
// 用户权限列表,根据用户拥有的权限标识与如 @PreAuthorize("hasAuthority('sys:menu:view')") 标注的接口对比,决定是否可以调用接口
// Set permissions = biUserService.findPermissions(username);
try {
roles = userService.getRoles(user.getId());
} catch (ManageException e) {
e.printStackTrace();
}
List grantedAuthorities = roles.stream().map(GrantedAuthorityImpl::new).collect(Collectors.toList());
return new JwtUserDetails(username, user.getPassword(), grantedAuthorities);
}
}
权限封装和安全用户模型 根据角色赋予不同的功能
package com.rkhd.ienterprise.apps.bi.security;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;
public class JwtUserDetails extends User {
private static final long serialVersionUID = 1L;
public JwtUserDetails(String username, String password, Collection extends GrantedAuthority> authorities) {
this(username, password, true, true, true, true, authorities);
}
public JwtUserDetails(String username, String password, boolean enabled, boolean accountNonExpired,
boolean credentialsNonExpired, boolean accountNonLocked, Collection extends GrantedAuthority> authorities) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
}
}
package com.rkhd.ienterprise.apps.bi.security;
import org.springframework.security.core.GrantedAuthority;
public class GrantedAuthorityImpl implements GrantedAuthority {
private static final long serialVersionUID = 1L;
private String authority;
public GrantedAuthorityImpl(String authority) {
this.authority = authority;
}
public void setAuthority(String authority) {
this.authority = authority;
}
@Override
public String getAuthority() {
return this.authority;
}
}
3.1 UserDetailsService
接口。该接口只提供了一个方法:
该方法很容易理解:通过用户名来加载用户。这个方法主要用于从系统数据中查询并加载具体的用户到Spring Security中。
3.2 UserDetails
从上面可以知道最终交给Spring Security的是。该接口是提供用户信息的核心接口。该接口实现仅仅存储用户的信息。后续会将该接口提供的用户信息封装到认证对象中去。默认提供了:
用户的权限集, 默认需要添加前缀
用户的加密后的密码, 不加密会使用前缀
应用内唯一的用户名
账户是否过期
账户是否锁定
凭证是否过期
用户是否可用
如果以上的信息满足不了你使用,你可以自行实现扩展以存储更多的用户信息。比如用户的邮箱、手机号等等。通常我们使用其实现类:
该类内置一个建造器会很方便地帮助我们构建对象,后面我们会用到它。
SpringSecurity-1-UserDetails接口
这个接口是我们自己用来定义用户表的结构的
SpringSecurity自己的用户信息只包含了Username,password,roles,假如我希望用户的实体类中还有性别sex字段,那么就没有办法了,所以SpringSecurity提供了UserDetails接口,我们可以自己新建一个包含sex字段的类,然后该类implements UserDetails接口,就可以获取我们说的这个sex字段
UserDetails实例是通过UserDetailsService接口的loadUserByUsername方法返回的,可以参考这篇文章创建自己的UserDetails(本文中包含sex的那个类)
UserDetails接口中有一个getAuthorities方法,这个方法返回的是权限,但是我们返回的权限必须带有“ROLE_”开头才可以,spring会自己截取ROLE_后边的字符串,也就是说,比如:我的权限叫ADMIN,那么,我返回告诉spring security的时候,必须告诉他权限是ROLE_ADMIN,这样spring security才会认为权限是ADMIN
示例代码:
假设一个用户拥有两个权限,一个叫权限1,一个叫权限2,下面的代码演示了如何使用UserDetails接口获取这两个权限,其本质就是实现getAuthorities()方法
public MyUser implements UserDetails{
// 这个GrantedAuthority是什么呢?其实它就是权限(Role),
// 因为本例中该用户有两个权限,所以这个Collection的长度是2
@Override
public Collection extends GrantedAuthority> getAuthorities(){
String role1="ROLE_权限1";
String role2="ROLE_权限2";
GrantedAuthority authority1=new SimpleGrantedAuthority(role1);
GrantedAuthority authority2=new SimpleGrantedAuthority(role2);
List list=new ArrayList<>();
list.add(authority1);
list.add(authority2);
return list;
}
}
根据UserDetails接口,我们知道spring更推荐将权限用逗号隔开的方式存到数据库user表中,而不推荐使用1对多的方式来存储权限(单独的一个user表和权限表,然后使用一个关系表将两个表关联起来)
参考链接
Spring Boot:整合Spring Security - 朝雨忆轻尘 - 博客园



