我使用Spring Boot 2,Spring Security进行了相对简单的设置,而且我正在使用JWT来保持用户登录。
完整的项目在这里:http://github.com/mikeycoxon/spring-boot-2-security-jwt
我有两个过滤器,一个是认证,另一个是授权。
我有一个AuthNFilter:
public class AuthNFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public AuthNFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest req,
HttpServletResponse res) throws AuthenticationException {
try {
User creds = new ObjectMapper()
.readValue(req.getInputStream(), User.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
creds.getUsername(),
creds.getPassword(),
creds.getRoles())
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
protected void successfulAuthentication(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain,
Authentication auth) throws IOException, ServletException {
String token = Jwts.builder()
.setSubject(((User) auth.getPrincipal()).getUsername())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS512, SECRET.getBytes())
.compact();
res.addHeader(HEADER_STRING, TOKEN_PREFIX + token);
}
}
这将针对数据存储验证用户,并使用令牌向响应中添加自定义标头。
和AuthZFilter:
public class AuthZFilter extends BasicAuthenticationFilter {
public AuthZFilter(AuthenticationManager authManager) {
super(authManager);
}
@Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain) throws IOException, ServletException {
String header = req.getHeader(HEADER_STRING);
if (header == null || !header.startsWith(TOKEN_PREFIX)) {
chain.doFilter(req, res);
return;
}
UsernamePasswordAuthenticationToken authentication = getAuthentication(req);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(req, res);
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
String token = request.getHeader(HEADER_STRING);
if (token != null) {
// parse the token.
String user = Jwts.parser()
.setSigningKey(SECRET.getBytes())
.parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
.getBody()
.getSubject();
if (user != null) {
return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
}
return null;
}
return null;
}
}
它取代了BasicAuthenticationFilter,以便我们可以读取JWT并在SecurityContext中设置用户。
为了应用这个,我设置了一个WebSecurityConfigurerAdapter,以便我们可以覆盖spring security的默认值:
@EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
private UserDetailsServiceImpl userDetailsService;
private BCryptPasswordEncoder bCryptPasswordEncoder;
public WebSecurity(UserDetailsServiceImpl userDetailsServiceImpl, BCryptPasswordEncoder bCryptPasswordEncoder) {
this.userDetailsService = userDetailsServiceImpl;
this.bCryptPasswordEncoder = bCryptPasswordEncoder;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable().authorizeRequests()
.antMatchers(SIGN_UP_URL).permitAll()
.antMatchers(LOGIN_URL).permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new AuthNFilter(authenticationManager()))
.addFilter(new AuthZFilter(authenticationManager()))
// this disables session creation on Spring Security
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
return source;
}
}
SIGNUP_URL
= / api / user并且是一个POST
LOGIN_URL
= spring自己的/登录端点
基本上,问题出现在测试中:
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("embedded")
@AutoConfigureMockMvc
public class AccessControllerFunctionalTest {
@Autowired
private WebApplicationContext context;
@Autowired
private MockMvc mvc;
@MockBean
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Before
public void setup() {
mvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
}
@Test
public void doSignup() throws Exception {
String requestString = "{\"username\": \"mike@gmail.com\",\"password\": \"password\"}";
mvc.perform(post("/api/user").contentType(APPLICATION_JSON)
.content(requestString))
.andDo(print()).andExpect(status().isOk());
}
@Test
public void doLoginFailsWithUserNotExists() throws Exception {
String requestString = "{\"username\": \"mike@gmail.com\",\"password\": \"password\"}";
mvc.perform(post("/login").contentType(APPLICATION_JSON)
.content(requestString))
.andDo(print())
.andExpect(status().isUnauthorized());
}
@Test
public void doLoginSuccessWithUserExists() throws Exception {
String requestString = "{\"username\": \"rmjcoxon@gmail.com\",\"password\": \"password\"}";
mvc.perform(post("/login").contentType(APPLICATION_JSON)
.content(requestString))
.andDo(print())
.andExpect(status().isOk())
.andExpect(header().exists(HEADER_STRING));
}
}
前两个测试通过,第三个测试失败,这是意料之外的。它始终返回:
MockHttpServletRequest:
HTTP Method = POST
Request URI = /login
Parameters = {}
Headers = {Content-Type=[application/json]}
Body = <no character encoding set>
Session Attrs = {}
Handler:
Type = null
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 401
Error message = Unauthorized
Headers = {X-Content-Type-Options=[nosniff], X-XSS-Protection=[1; mode=block], Cache-Control=[no-cache, no-store, max-age=0, must-revalidate], Pragma=[no-cache], Expires=[0], X-Frame-Options=[DENY]}
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
2018-05-27 19:56:24.868 INFO 8949 --- [ Test worker] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring FrameworkServlet ''
2018-05-27 19:56:24.868 INFO 8949 --- [ Test worker] o.s.t.web.servlet.TestDispatcherServlet : FrameworkServlet '': initialization started
2018-05-27 19:56:24.872 INFO 8949 --- [ Test worker] o.s.t.web.servlet.TestDispatcherServlet : FrameworkServlet '': initialization completed in 4 ms
MockHttpServletRequest:
HTTP Method = POST
Request URI = /login
Parameters = {}
Headers = {Content-Type=[application/json]}
Body = <no character encoding set>
Session Attrs = {}
Handler:
Type = null
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 401
Error message = Unauthorized
Headers = {X-Content-Type-Options=[nosniff], X-XSS-Protection=[1; mode=block], Cache-Control=[no-cache, no-store, max-age=0, must-revalidate], Pragma=[no-cache], Expires=[0], X-Frame-Options=[DENY]}
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
Status expected:<200> but was:<401>
Expected :200
Actual :401
我不确定/ login端点来自哪里,但我很确定它不应该像它一样进行身份验证,否则有人如何登录?
我认为我对Spring Security缺乏了解是她的错,谁能看到我做错了什么?
之前我在一个不同的设置上问了一个类似的问题 - 答案中的答案很少,所以我再试一次。
答案 0 :(得分:3)
默认情况下,spring会生成基本表单登录。您需要在Websecurity中禁用它,如下所示:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable().authorizeRequests()
.antMatchers(SIGN_UP_URL).permitAll()
.antMatchers(LOGIN_URL).permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new AuthNFilter(authenticationManager()))
.addFilter(new AuthZFilter(authenticationManager()))
// this disables session creation on Spring Security
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().formLogin().disable();
}
编辑: 经过一些调试后,我发现了错误。
您已经嘲笑UserRepository
但不是方法
findByUsername
将始终返回null。我把它删除了
使用针对hsql的真实存储库。
用户始终处于锁定状态。
@Override
public boolean isAccountNonLocked() {
return false; //changed it to true
}
更改这些后,测试运行没有错误。