一直以来写Web应用时对于身份的验证和授权都比较简单,借助session attribute,又或者使用自己实现的认证拦截器(filter、interceptor……)等。
但其实spring security有提供一整套根据不同标准制定的处理认证授权的方法,而且还支持自定义认证。这次要讲的是通过自定义认证来实现JWT,关于spring security的更详细讲解还需要准备一下待下篇。
# - DEMO 目录结构
目录如下
- src/main/java
- indi.a9043.demo
- config
- SecurityConfig.java
- controller
- TestController.java
- entity
- TestUser.java
- security
- TestAccessDeniedHandler.java
- TestAuthenticationEntryPoint.java
- TestAuthenticationFailureHandler.java
- TestAuthenticationFilter.java
- TestAuthenticationSuccessHandler.java
- service
- TestUserDetailsService.java
- util
- JwtUtil.java
- DemoApplication.java
- config
- indi.a9043.demo
如上表,
- config 为项目配置类,demo只有一个security配置
- controller 测试的接口
- entity 认证用的实体类
- security 自定义认证相关类
- service 业务类,demo包含身份验证的Service
- util 工具类,demo包含JWT使用工具类
- DemoApplication main函数入口
# - 开始构建
本项目使用spring boot initializer预先添加,项目依赖和入口类,pom.xml 依赖如下,可以自行添加。
<!-- pom.xml -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20180130</version>
</dependency>
</dependencies>
# - 新建用户实体以及用户操作类
在entity包下创建TestUser 类并实现UserDetails接口
package indi.a9043.demo.entity; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; /** * 仅考虑简单认证 */ public class TestUser implements UserDetails { private String userName; private String userPassword; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } @Override public String getPassword() { return this.userPassword; } @Override public String getUsername() { return this.userName; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } //getter & setter public String getUserName() { return userName; } public TestUser setUserName(String userName) { this.userName = userName; return this; } public String getUserPassword() { return userPassword; } public TestUser setUserPassword(String userPassword) { this.userPassword = userPassword; return this; } }
在service包下新建TestUserDetailsService 并实现UserDetailsService
package indi.a9043.demo.service; import indi.a9043.demo.entity.TestUser; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; @Service public class TestUserDetailsService implements UserDetailsService { /* * 定义一个用户用来测试 * 用户名: test1 * 密码: 123456 */ private TestUser testUser; public TestUserDetailsService() { testUser = new TestUser(); testUser.setUserName("test1"); testUser.setUserPassword(new BCryptPasswordEncoder().encode("123456")); } /** * 根据用户名查询UserDetails * * @param username 用户名 * @return UserDetails * @throws UsernameNotFoundException 无此用户名 */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { if (username.equals(testUser.getUserName())) { return testUser; } throw new UsernameNotFoundException("User " + username + " was not found"); } }
# - 新建JwtUtil 工具类
util包下
package indi.a9043.demo.util;
import io.jsonwebtoken.*;
import org.apache.tomcat.util.codec.binary.Base64;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Calendar;
import java.util.Date;
import java.util.Map;
import java.util.Optional;
/**
* Token Util
*
* @author a9043
*/
public class JwtUtil {
private static SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS512;
private static String stringKey = "a9043_xxx";
private static byte[] encodedKey = Base64.encodeBase64(stringKey.getBytes());
private static SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
/**
* 生成Token
*
* @param claims 声明
* @return token
*/
public static String createJWT(Map<String, Object> claims) {
return createJWT(claims, Calendar.HOUR, 2);
}
/**
* 自定义过期时间Token
*
* @param claims 声明
* @param expireField 时间位
* @param expireAmount 时间长
* @return token
*/
public static String createJWT(Map<String, Object> claims, int expireField, int expireAmount) {
Date now = Calendar.getInstance().getTime();
Calendar expireCal = Calendar.getInstance();
expireCal.add(expireField, expireAmount);
Date expire = expireCal.getTime();
JwtBuilder builder = Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
.setHeaderParam("alg", "HS512")
.setIssuedAt(now)
.setClaims(claims)
.setIssuer("a9043")
.setExpiration(expire)
.signWith(signatureAlgorithm, key);
return builder.compact();
}
/**
* Token parser
*
* @param JwtStr token
* @return claims
* @throws SignatureException err
* @throws ExpiredJwtException err
*/
public static Claims parseJwt(String JwtStr) throws SignatureException, ExpiredJwtException {
Claims claims = Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(JwtStr)
.getBody();
return Optional
.ofNullable(claims)
.filter(claim -> claim.getExpiration() == null || !new Date().after(claim.getExpiration()))
.orElse(null);
}
}
# - 新建测试接口
controller 包下
@RestController
public class TestController {
/**
* 不受保护资源
*
* @return
*/
@GetMapping("/hello")
public String hello() {
return "hello";
}
/**
* 受保护资源
*
* @return
*/
@GetMapping("/resource")
public String resource() {
return "resource";
}
}
# - 新建token过滤器
security包下新建 TestAuthenticationFilter 继承 OncePerRequestFilter
package indi.a9043.demo.security;
import indi.a9043.demo.service.TestUserDetailsService;
import indi.a9043.demo.util.JwtUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class TestAuthenticationFilter extends OncePerRequestFilter {
@Resource
private TestUserDetailsService testUserDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
//获得header token
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String token = header.substring(7);
Claims claims;
if (token.length() <= 0 ||
SecurityContextHolder.getContext().getAuthentication() != null) {
return;
}
//解析token
try {
claims = JwtUtil.parseJwt(token);
String userName = (String) claims.get("userName");
UserDetails userDetails = testUserDetailsService.loadUserByUsername(userName);
if (!userDetails.getUsername().equals(userName)) {
SecurityContextHolder.clearContext();
filterChain.doFilter(request, response);
return;
}
//设定Authentication
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails.getUsername(),
userDetails.getPassword(),
userDetails.getAuthorities());
authentication.setDetails(
new WebAuthenticationDetailsSource().
buildDetails(
request));
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (MalformedJwtException | SignatureException | ExpiredJwtException e) {
SecurityContextHolder.clearContext();
}
filterChain.doFilter(request, response);
}
}
# - 新建各处理类
都新建在security包下
TestAuthenticationSuccessHandler 登录成功
package indi.a9043.demo.security; import indi.a9043.demo.entity.TestUser; import indi.a9043.demo.util.JwtUtil; import org.json.JSONObject; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.HashMap; import java.util.Map; @Component public class TestAuthenticationSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { TestUser testUser = (TestUser) authentication.getPrincipal(); Map<String, Object> claimsMap = new HashMap<>(); claimsMap.put("userName", testUser.getUserName()); String token = JwtUtil.createJWT(claimsMap); JSONObject jsonObject = new JSONObject(); jsonObject.put("access-token", token); response.getWriter().write(jsonObject.toString()); } }
TestAuthenticationFailureHandler 登录失败
package indi.a9043.demo.security; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class TestAuthenticationFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { response.getWriter().write("failure"); } }
TestAccessDeniedHandler 登录后的拒绝handler
package indi.a9043.demo.security; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class TestAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { response.getWriter().write("denied"); } }
TestAuthenticationEntryPoint 未认证入口
package indi.a9043.demo.security; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class TestAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage()); } }
# - 配置 SecurityConfig
此为web security 配置类,不同版本和构建的spring可能需要带上 @EnableWebSecurity 注解
package indi.a9043.demo.config;
import indi.a9043.demo.security.*;
import indi.a9043.demo.service.TestUserDetailsService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.annotation.Resource;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private TestUserDetailsService testUserDetailsService;
@Resource
private TestAuthenticationEntryPoint testAuthenticationEntryPoint;
@Resource
private TestAuthenticationSuccessHandler testAuthenticationSuccessHandler;
@Resource
private TestAuthenticationFailureHandler testAuthenticationFailureHandler;
@Resource
private TestAccessDeniedHandler testAccessDeniedHandler;
@Resource
private DaoAuthenticationProvider daoAuthenticationProvider;
@Resource
private TestAuthenticationFilter testAuthenticationFilter;
@Bean
DaoAuthenticationProvider daoAuthenticationProvider() {
/*
* 自定义Provider
* 自定义UserDetailsService
* 自定义PasswordEncoder
*/
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(testUserDetailsService);
daoAuthenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder());
return daoAuthenticationProvider;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
/*
* add 自定义provider
*/
auth
.authenticationProvider(daoAuthenticationProvider);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable() // 关闭csrf保护
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 设置为无状态
.and()
.authorizeRequests()
.antMatchers("/hello").permitAll() // 不受保护资源
.anyRequest().authenticated() //受保护资源
.and()
.formLogin() //表单登录
.loginProcessingUrl("/login") //表单登录api 不同于loginPage登录页面
.successHandler(testAuthenticationSuccessHandler) //登录成功handler
.failureHandler(testAuthenticationFailureHandler) //登录失败handler
.and()
.exceptionHandling()
.authenticationEntryPoint(testAuthenticationEntryPoint) //未授权入口
.accessDeniedHandler(testAccessDeniedHandler) //拒绝入口
.and()
.addFilterBefore(testAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class); //添加token过滤, 处理带token请求
}
}
# - 测试
综上,已经security 已经配置好了,现在开始测试 rest api
- 访问不受保护资源 /hello
- req
GET http://localhost:8080/hello Accept: */* Cache-Control: no-cache ###
- res
hello
- 访问受保护资源 /resource
- req
```http
GET http://localhost:8080/resource
Accept: */*
Cache-Control: no-cache
###
- res
{
"timestamp": "2018-07-22T13:53:31.134+0000",
"status": 401,
"error": "Unauthorized",
"message": "Full authentication is required to access this resource",
"path": "/resource"
}
登录接口 /login
- req
POST http://localhost:8080/login?username=test1&password=123456 Accept: */* Cache-Control: no-cache ###
- res
{ "access-token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJhOTA0MyIsInVzZXJOYW1lIjoidGVzdDEiLCJleHAiOjE1MzIyNzQ0MjR9.sKfslwlYyn8lKZ0UQdNqKdBB2aheoufs5O2p3brUxxKyn5cAN_0hOqCB7LkmhdMVe4XIwpjAiez48Gqwqb8AIQ" }
访问受保护资源 /resource
- req
GET http://localhost:8080/resource Accept: */* Cache-Control: no-cache Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJhOTA0MyIsInVzZXJOYW1lIjoidGVzdDEiLCJleHAiOjE1MzIyNzQ0MjR9.sKfslwlYyn8lKZ0UQdNqKdBB2aheoufs5O2p3brUxxKyn5cAN_0hOqCB7LkmhdMVe4XIwpjAiez48Gqwqb8AIQ ###
- res
resource
# - 总结
没有总结。。自此,我总算尝试了通过Spring Security,使用自定义方式进行身份认证。
***
demo 源代码托管在github: //github.com/190434957/spring_security_JWT_DEMO> ",