若是不是先后端分离项目,使用SpringSecurity作登陆功能会很省心,只要简单的几项配置,即可以轻松完成登陆成功失败的处理,当访问须要认证的页面时,能够自动重定向到登陆页面。可是先后端分离的项目就不同了,不能直接由后台处理,而是要向前端返回相应的json提示。前端
在本例的介绍中,主要解决了如下几个问题:java
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
package com.hanstrovsky.config; ... /** * @author Hanstrovsky */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) // security默认不支持注解的方式的权限控制,加上这个注解开启 public class WebSecurityConfig extends WebSecurityConfigurerAdapter { private final MyUserDetailsService myUserDetailsService; private final MyPasswordEncoder myPasswordEncoder; public WebSecurityConfig(MyUserDetailsService myUserDetailsService, MyPasswordEncoder myPasswordEncoder) { this.myUserDetailsService = myUserDetailsService; this.myPasswordEncoder = myPasswordEncoder; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 定义加密解密方式 auth.userDetailsService(myUserDetailsService).passwordEncoder(myPasswordEncoder); } @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .httpBasic() // 访问须要认证的url,进行json提示 .and().exceptionHandling() .authenticationEntryPoint((req, resp, e) -> { resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); FrontResult frontResult = FrontResult.init(FrontResult.LOGIN, "未登陆或登陆超时!"); out.write(new ObjectMapper().writeValueAsString(frontResult)); out.flush(); out.close(); }) .and() .authorizeRequests() .anyRequest().authenticated()// 必须认证以后才能访问 .and() .formLogin()// 表单登陆 .permitAll() // 和表单登陆相关的接口通通都直接经过 .and() .logout().deleteCookies("JSESSIONID")// 注销登陆,删除cookie // 自定义注销成功,返回json .logoutSuccessHandler(new LogoutSuccessHandler() { @Override public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException { resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); FrontResult frontResult = FrontResult.init(FrontResult.SUCCEED, "注销成功!"); out.write(new ObjectMapper().writeValueAsString(frontResult)); out.flush(); out.close(); } }) .and() // session 超时返回json提示 .sessionManagement() .maximumSessions(5).maxSessionsPreventsLogin(true)// 同一用户最大同时在线数量5个,超出后阻止登陆 // session 超时返回json提示 .expiredSessionStrategy(new SessionInformationExpiredStrategy() { @Override public void onExpiredSessionDetected( SessionInformationExpiredEvent sessionInformationExpiredEvent) throws IOException, ServletException { HttpServletResponse resp = sessionInformationExpiredEvent.getResponse(); // 返回提示 resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); FrontResult frontResult = FrontResult.init(FrontResult.LOGIN, "登陆超时!"); out.write(new ObjectMapper().writeValueAsString(frontResult)); out.flush(); out.close(); } }); //用重写的Filter替换掉原有的UsernamePasswordAuthenticationFilter http.addFilterAt(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);} //注册自定义的UsernamePasswordAuthenticationFilter,使用json格式数据登陆 @Bean CustomAuthenticationFilter customAuthenticationFilter() throws Exception { CustomAuthenticationFilter filter = new CustomAuthenticationFilter(); // 自定义登陆成功或失败 返回json提示 filter.setAuthenticationSuccessHandler((req, resp, authentication) -> { resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); FrontResult frontResult = FrontResult.init(FrontResult.SUCCEED, "登陆成功!"); out.write(new ObjectMapper().writeValueAsString(frontResult)); out.flush(); out.close(); }); filter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() { @Override public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException { resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); String errorMessage = "登陆失败"; FrontResult frontResult = FrontResult.init(FrontResult.FAILED, errorMessage); out.write(new ObjectMapper().writeValueAsString(frontResult)); out.flush(); out.close(); } }); filter.setFilterProcessesUrl("/user/login"); //重用WebSecurityConfigurerAdapter配置的AuthenticationManager,否则要本身组装AuthenticationManager filter.setAuthenticationManager(authenticationManagerBean()); return filter; } }
security默认提供了Basic和表单两种登陆方式,不支持Json格式的数据,须要对处理登陆的过滤器进行修改。这里,咱们重写了UsernamePasswordAuthenticationFilter的attemptAuthentication方法。web
package com.hanstrovsky.filter; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.InputStream; import java.util.Map; /** * 自定义过滤器,重写 attemptAuthentication方法,实现使用json格式的数据进行登陆 * * @author Hanstrovsky */ @Slf4j public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter { @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) { ObjectMapper mapper = new ObjectMapper(); UsernamePasswordAuthenticationToken authRequest = null; try (InputStream is = request.getInputStream()) { Map<String, String> authenticationBean = mapper.readValue(is, Map.class); String username = authenticationBean.get("username"); String password = authenticationBean.get("password"); authRequest = new UsernamePasswordAuthenticationToken( username, password); } catch (IOException e) { e.printStackTrace(); authRequest = new UsernamePasswordAuthenticationToken( "", ""); } setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } else { // 保留原来的方法 return super.attemptAuthentication(request, response); } } }
这个接口是用来提供用户名和密码的,能够经过查询数据库获取用户。本例直接在代码中写死。spring
package com.hanstrovsky.service; import com.hanstrovsky.entity.MyUserDetails; 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.Repository; /** * @author Hanstrovsky */ @Repository public class MyUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException { // 能够在此处自定义从数据库查询用户 MyUserDetails myUserDetail = new MyUserDetails(); myUserDetail.setUsername(username); myUserDetail.setPassword("123456"); return myUserDetail; } }
自定义密码的加密方式。数据库
package com.hanstrovsky.util; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; /** * 自定义的密码加密方法,实现了PasswordEncoder接口 * * @author Hanstrovsky */ @Component public class MyPasswordEncoder implements PasswordEncoder { @Override public String encode(CharSequence charSequence) { //加密方法能够根据本身的须要修改 return charSequence.toString(); } @Override public boolean matches(CharSequence charSequence, String s) { return encode(charSequence).equals(s); } }
这个类是用来存储登陆成功后的用户数据,security提供了直接获取用户信息的接口json
package com.hanstrovsky.entity; import lombok.Getter; import lombok.Setter; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; import java.util.Collection; /** * 实现UserDetails,可自定义添加更多属性 * * @author Hanstrovsky */ @Getter @Setter @Component public class MyUserDetails implements UserDetails { //登陆用户名 private String username; //登陆密码 private String password; private Collection<? extends GrantedAuthority> authorities; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return this.authorities; } private boolean accountNonExpired = true; private boolean accountNonLocked = true; private boolean credentialsNonExpired = true; private boolean enabled = true; }
以上,即可以实现先后端分离项目基本的登陆功能。后端