一、任务描述
有一个测试接口AuthTestController
@RestController
@RequestMapping("/auth")
public class AuthTestController {
@GetMapping()
public String greeting() {
return "Hello,Auth!";
}
}
现在用Postman测试一下,如图:

任何人只要知道了这个接口的地址,那么就可以在任何时候无障碍地访问这个接口并得到期望的返回值,这显然是不安全的,所以需要给这个接口加上权限控制,访问该接口的用户必须先授权,授权后再通过授权号来访问该接口。具体实现如下
二、添加Spring Security依赖
compile group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: '2.0.5.RELEASE'
添加了Spring Security依赖后,我们再来访问该接口发现提示401,说明接口已经被保护起来了,需要授权才能正常访问

接下来我们就来添加提供授权的代码
三、编写提供授权的代码
3.1 继承WebSecurityConfigurerAdapter
@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final String DEV_ENVIRONMENT = "dev";
/**
* 运行环境:dev/prod/test
*/
@Value("${spring.profiles.active}")
private String active;
/**
* 密码加密及校验方式
*
* @return
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* Web资源权限控制
*
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
web.ignoring().antMatchers("/config/**", "/css/**", "/fonts/**", "/img/**", "/js/**");
//Ant Design登录页面,限定GET,避免和 Spring Security 的login(POST方式)冲突
web.ignoring().antMatchers(HttpMethod.GET,"/login");
//Ant Design 页面
web.ignoring().antMatchers("/","/console", "/console/**","/static/**","/*.png","/*.js","/*.css");
//swagger-ui start
web.ignoring().antMatchers("/v2/api-docs/**");
web.ignoring().antMatchers("/swagger.json");
web.ignoring().antMatchers("/swagger-ui.html");
web.ignoring().antMatchers("/swagger-resources/**");
web.ignoring().antMatchers("/webjars/**");
//swagger-ui end
}
/**
* HTTP请求权限控制
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//本地开发环境关闭权限控制,方便测试
if(DEV_ENVIRONMENT.equals(active)){
http.cors().and().csrf().disable().authorizeRequests()
.antMatchers("/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new JwtLoginFilter(authenticationManager()))
.addFilter(new JwtAuthenticationFilter(authenticationManager()));
}else{
http.cors().and().csrf().disable().authorizeRequests()
.antMatchers("/user-login/verify-account").permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new JwtLoginFilter(authenticationManager()))
.addFilter(new JwtAuthenticationFilter(authenticationManager()));
}
// 禁用 SESSION、JSESSIONID
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
注意:
.antMatchers("/user-login/verify-account").permitAll() 要写在.anyRequest().authenticated()前面,不然接口权限放行会无效
3.2 继承BasicAuthenticationFilter
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith(JwtUtils.getAuthorizationHeaderPrefix())) {
chain.doFilter(request, response);
return;
}
UsernamePasswordAuthenticationToken authenticationToken = getUsernamePasswordAuthenticationToken(header);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
chain.doFilter(request, response);
}
private UsernamePasswordAuthenticationToken getUsernamePasswordAuthenticationToken(String token) {
String user = Jwts.parser()
.setSigningKey("PrivateSecret") //私钥
.parseClaimsJws(token.replace(JwtUtils.getAuthorizationHeaderPrefix(), ""))
.getBody()
.getSubject();
if (null != user) {
return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
}
return null;
}
}
3.3 继承UsernamePasswordAuthenticationFilter
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public JwtLoginFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
username,
password,
new ArrayList<>()
)
);
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, Authentication authResult) {
String token = Jwts.builder()
.setSubject(((User) authResult.getPrincipal()).getUsername())
.setExpiration(new Date(System.currentTimeMillis() + 30 * 60 * 1000))
.signWith(SignatureAlgorithm.HS512, "PrivateSecret") //私钥
.compact();
returnToken(response, JwtUtils.getTokenHeader(token));
}
private void returnToken(HttpServletResponse response, String token) {
JwtToken jwtToken = new JwtToken(token);
JSONObject responseJSONObject = new JSONObject(jwtToken);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
PrintWriter out = null;
try {
out = response.getWriter();
out.append(responseJSONObject.toString());
out.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (out != null) {
out.close();
}
}
}
}
3.4 实现接口UserDetailsService
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
//TODO 从数据库取数据
String password = "$2a$10$hoIKMK7haFkAShKNHctxceBSCigIFOkrjOh7XNDF8s0py14RNVkXW"; //admin BCryptPasswordEncoder加密后的字符串
//String password = userServiceImp.getUserPassWord(userName);
return new User(userName, password, getAuthority()); //emptyList()
}
private List getAuthority() {
return Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN"));
}
}
3.5 JwtUtils
public class JwtUtil {
private static final String AUTHORIZATION_HEADER_PREFIX = "Bearer ";
public static String getTokenHeader(String rawToken) {
return AUTHORIZATION_HEADER_PREFIX + rawToken;
}
public static String getAuthorizationHeaderPrefix() {
return AUTHORIZATION_HEADER_PREFIX;
}
}
3.6 JwtToken
public class JwtToken implements Serializable {
private String token;
public JwtToken(String token) {
this.token = token;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
}
四、获得密码加密字符串
现假设账号密码都为admin,因为在上面的代码里面我们的密码加密及校验方式用的是BCryptPasswordEncoder,所以先手动获取一下admin加密后的字符串
public class Client {
public static void main(String[] args) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String pwd = encoder.encode("admin");
System.out.println(pwd);
}
}
得到加密后的字符串
$2a$10$hoIKMK7haFkAShKNHctxceBSCigIFOkrjOh7XNDF8s0py14RNVkXW
将该加密后的字符串写死在UserDetailsService 实现类的loadUserByUsername方法里,正常应该是从数据通过用户名取出密码的,现为了简化测试,先手动写死。
五、通过账号、密码获取授权
通过POST方式,访问login接口得到授权号,注意要传参数用户名和密码

login方法是Spring Security内置的一个授权接口,查看源码如下

六、通过授权号访问测试接口
将刚才的授权号拷贝出来(不用拷贝前缀Bearer)
eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTU2MDQxNzE1OH0.5wGV19nJR2HX6fB_2GdLlb6Q8khCA-6a9tyAOJXxpuuIProTCU3keLeFTrBQrowoOu_6dUs4Uz9uznC5eXy_sA
Authorization Type 选择 Bearer Token,并在Token输入框内输入刚才的授权号,发现能正常访问测试接口了,如下图:

七、SecurityConfiguration具体介绍
现在我们重点来具体介绍一下SecurityConfiguration里相关代码的作用
7.1 Spring Security禁用session
我们先注释以下代码
//http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
我们首先传入授权号访问测试接口,测试结果是能正常访问,现在我们再来尝试一下不传人授权号来访问测试接口,看有什么反应,测试结果如下:

发现很神奇,不传授权号仍然能访问测试接口,到底是哪里出了问题?我们点开Postman窗口右上角的“Cookies”发现有JSESSIONID(session的一种),JSESSIONID是Spring Boot内嵌Tomcat生成的,就是这个JSESSIONID已经记录了我们上一次请求的信息,所以现在不传人授权号,仍然可以访问到测试接口

我们现在手动把JSESSIONID删除,再来测试看一下,发现已经提示403 访问失败,请求被Forbidden了,接口得到很好的保护,如下图:

我们现在用Postman测试可以手动删除JSESSIONID,那如果是其他的client(如web、android等)访问也会有这个问题,有没有什么办法让Spring Boot内嵌Tomcat不生产这个JSESSIONID?答案是:有办法。
刚才注释的那段代码就是用来禁用 SESSION、JSESSIONID的,我们把注释的代码打开再来测试,发现Cookies里就没有生成JSESSIONID了。
7.2 访问swagger
我们先注释以下代码
// web.ignoring().antMatchers("/v2/api-docs/**");
// web.ignoring().antMatchers("/swagger.json");
// web.ignoring().antMatchers("/swagger-ui.html");
// web.ignoring().antMatchers("/swagger-resources/**");
// web.ignoring().antMatchers("/webjars/**");
如果接入了swagger作为接口文档,当添加Spring Security之后,发现之前能正常访问的接口文档现在访问不了了,原因是Spring Security对swagger的访问也被加上了权限控制,如下图:

swagger一般只在本地开发或内部测试环境中使用,在生成环境会被关闭,所以我们期待swagger不要被权限控制(开发环境开启Swagger,生产环境关闭Swagger),刚才我们注释的那段代码就是用来配置swagger不要被权限控制,我们打开注释代码,重新启动服务器,访问swagger,如下图:

7.3 本地开发环境关闭权限校验
我们期望如果是本地开发环境则关闭权限校验,因为这样方便我们通过Postman测试接口,只有测试环境和生产环境时才开启全新校验。我们可以这样实现:
首先,我的配置文件有三套,分别对应本地开发环境、测试环境、生产环境,在application.yml里配置采用哪套配置文件,我们可以通过spring.profiles.active这个参数来取到当前的运行环境,然后设置是否开启权限校验,所以我们在SecurityConfiguration的方法configure(HttpSecurity http)里添加了一段代码,代码如下:

//本地开发环境关闭权限控制,方便测试
if("dev".equals(active)){
http.cors().and().csrf().disable().authorizeRequests()
.antMatchers("/**").permitAll();
}else{
http.cors().and().csrf().disable().authorizeRequests()
.anyRequest().authenticated()
.and()
.addFilter(new JwtLoginFilter(authenticationManager()))
.addFilter(new JwtAuthenticationFilter(authenticationManager()));
}
网友评论