springboot springsecurity 基于html 通过 json 方式登录 + token 验证 + remember me
数据库

pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<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>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Autowired
private DataSource dataSource;
String [] urls = {"/**/*.html","/js/**","/logon"};
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(urls)
.permitAll()
.anyRequest()
.authenticated()
.and()
.rememberMe() //添加后会走RememberMeAuthenticationFilter 在所有认证失败后去通过cookie 认证
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(60 * 60 * 60) // 添加后会走RememberMeAuthenticationFilter 需要知道过期时间
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().securityContext() //不使用session
.and()
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
//添加Token处理的过滤器
http.addFilterAfter(tokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
//rememer-me tomken持久化的bean
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl persistentTokenRepository = new JdbcTokenRepositoryImpl();
persistentTokenRepository.setDataSource(dataSource);
// persistentTokenRepository.setCreateTableOnStartup(true); 是否自动创建表
return persistentTokenRepository;
}
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//auth.inMemoryAuthentication().withUser("admin").password(NoOpPasswordEncoder.getInstance().encode("admin")).roles("USER");
//添加token 处理的认证
auth.userDetailsService(userService).passwordEncoder(bCryptPasswordEncoder()).and().authenticationProvider(new TokenVerifyPovider());
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public TokenFilter tokenFilter() throws Exception {
return new TokenFilter(authenticationManagerBean(),urls);
}
//自定义在登录成功后手动将Remeber token 保存到数据库
@Bean
public RemeberMeFilter remeberMeFilter(){
return new RemeberMeFilter("remeber-me", userService, persistentTokenRepository(),60 * 60 * 60);
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
}
@Service
public class UserService implements UserDetailsService {
@Autowired
private UserDao userDao;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
User user = userDao.queryByUsername(s);
UserDetails userDetails = null;
if(Optional.ofNullable(user).isPresent()){
return new org.springframework.security.core.userdetails.User(s,user.getPassword(),true,true,true,true,user.getAuthorities());
}
return userDetails;
}
}
@Service
public class ValidateLoginInfoService {
@Autowired
AuthenticationManager authenticationManager;
@Autowired
RemeberMeFilter remeberMeFilter;
public boolean validateUser(String username, String password, String rememberMe,HttpServletRequest request, HttpServletResponse response){
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username,password);
Authentication authentication = null;
try {
authentication = authenticationManager.authenticate(token);
} catch (AuthenticationException e) {
e.printStackTrace();
return false;
}
SecurityContextHolder.getContext().setAuthentication(authentication);
// rememberMeServices.loginSuccess(request,response,authentication);
//登录验证成功后 手动执行将remember token保存到数据库
remeberMeFilter.loginSuccess2(request, response,authentication,rememberMe);
return true;
}
}
public class StatelessTokenAuthentication extends AbstractAuthenticationToken {
private HttpServletRequest request;
private HttpServletResponse response;
public StatelessTokenAuthentication (HttpServletRequest request, HttpServletResponse response){
super((Collection)null);
this.request = request;
this.response = response;
}
public StatelessTokenAuthentication(Object object){
super((Collection)null);
}
@Override
public Object getCredentials() {
return new Credentials();
}
@Override
public Object getPrincipal() {
return null;
}
class Credentials {
public HttpServletRequest getRequest(){
return StatelessTokenAuthentication.this.request;
}
public HttpServletResponse getResponse(){
return StatelessTokenAuthentication.this.response;
}
}
}
public class TokenVerifyPovider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//return new StatelessTokenAuthentication("success"); // 本例子中混合了token 认证和remember me认证,只要 token认证通过了 rememberme 认证就不会进行,所以只要返回null就可以进行 rememer认证,如果 只想token 认证成功返回注释的信息即可,需要返回什么自己扩展
return null;
}
@Override
public boolean supports(Class<?> authentication) {
return ClassUtils.isAssignable(StatelessTokenAuthentication.class, authentication);
}
}
@Entity
@Data
public class User implements UserDetails {
@Id
@GeneratedValue
private Integer id;
@Column(name = "name")
private String username;
private String password;
private String role;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> roleList = new ArrayList<GrantedAuthority>();
roleList.add(new SimpleGrantedAuthority("ROLE_" + this.role));
return roleList;
}
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return false;
}
}
@RestController
public class LoginController {
@Autowired
private ValidateLoginInfoService validateLoginInfoService;
@Autowired
private HttpServletRequest request;
@Autowired
private HttpServletResponse response;
@PostMapping("logon")
public Object login(@RequestBody Map<String,String> map){
String username = map.get("username");
String password = map.get("password");
String rememberMe = map.get("remember-me");
boolean flag = validateLoginInfoService.validateUser(username,password,rememberMe,request,response);
Map<String,String> map3 = new HashMap<String,String>(){{
put("name",username);
}};
String token = JwtUtil.createToken(map3);
Map<String,String> map2 = new HashMap<>();
map2.put("status","success");
map2.put("token",token);
if(!flag){
map2.put("status","error");
}
return map2;
}
@PostMapping("main")
/// @PreAuthorize("hasRole('test')")
public Object main(){
Map<String,String> map2 = new HashMap<>();
map2.put("status","success");
return map2;
}
}
public interface UserDao extends JpaRepository<User, Integer> {
public User queryByUsername(String username);
@Query("select u from User u where u.username = ?1 and u.password = ?2")
public User queryByUsernameAndPassword(String name,String pwd);
}
//至于为什么继承PersistentTokenBasedRememberMeServices 请百度rememberme的原理,PersistentTokenBasedRememberMeServices 是最后持久化remember me token的类
public class RemeberMeFilter extends PersistentTokenBasedRememberMeServices {
public RemeberMeFilter(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {
super(key, userDetailsService, tokenRepository);
}
public RemeberMeFilter(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository,int tokenValiditySeconds) {
super(key, userDetailsService, tokenRepository);
this.setTokenValiditySeconds(tokenValiditySeconds);
}
public void loginSuccess2(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication,String rememberMe) {
if (rememberMe != null && Boolean.valueOf(rememberMe)) {
this.onLoginSuccess(request, response, successfulAuthentication);
}
}
}
//自定义过滤器不回忽略permitAll()的url, 这个查过原因好像是security issues ,需要自己处理这些url进行忽略
public class TokenFilter extends OncePerRequestFilter {
private AuthenticationManager authenticationManager;
private String [] urls ;
public TokenFilter(AuthenticationManager authenticationManager, String[] urls) {
this.authenticationManager = authenticationManager;
this.urls = urls;
}
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
FilterInvocation fi = new FilterInvocation(httpServletRequest,httpServletResponse,filterChain);
String requestUrl = fi.getRequestUrl();
List<RequestMatcher> list = new ArrayList<>();
Stream.of(this.urls).forEach(url -> list.add(new AntPathRequestMatcher(url)));
OrRequestMatcher requestMatcher = new OrRequestMatcher(list);
boolean flag = requestMatcher.matches(httpServletRequest);
if(flag || httpServletRequest.getMethod().equals(HttpMethod.GET.toString())) {
super.doFilter(httpServletRequest, httpServletResponse, filterChain);
}else{
try {
StatelessTokenAuthentication statelessTokenAuthentication = new StatelessTokenAuthentication(httpServletRequest,httpServletResponse);
Authentication authentication = authenticationManager.authenticate(statelessTokenAuthentication);
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (AuthenticationException e) {
e.printStackTrace();
}
super.doFilter(httpServletRequest, httpServletResponse, filterChain);
}
}
}
public class JwtUtil {
private final static String secret = "abcdefg";
// iss: jwt签发者
// sub: jwt所面向的用户
// aud: 接收jwt的一方
// exp: jwt的过期时间,这个过期时间必须要大于签发时间
// nbf: 定义在什么时间之前,该jwt都是不可用的.
// iat: jwt的签发时间
// jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
public static String createToken(Map claims){
return Jwts.builder().setClaims(claims)
.setExpiration(DateUtils.plusDays(1))
.setId(Objects.toString(DateUtils.getSecond()))
.setIssuedAt(DateUtils.getCurrentDate()) //发布时间
.setSubject("authentication token")
.setIssuer("test")
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public static Claims parseToken(String token){
Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
return claims;
}
}
}
public class DateUtils {
public static Date getCurrentDate(){
LocalDateTime dateTime = LocalDateTime.now();
return Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant());
}
public static Date plusDays(int day){
LocalDateTime dateTime = LocalDateTime.now();
dateTime = dateTime.plusDays(day);
return Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant());
}
public static long getSecond(){
LocalDateTime dateTime = LocalDateTime.now();
return dateTime.getSecond();
}
}
application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/test?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.properties.hibernate.hbm2ddl.auto=update
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="js/jquery-3.4.1.min.js"></script>
<script>
$(function(){
$("#sub").click(function(){
var username = $("#username").val();
var password = $("#password").val();
var remeberMe = $("#remember-me").val();
var token = document.cookie.split('; ').reduce(function(result, v, i, a) { var k = v.split('='); result[k[0]] = k[1]; return result; }, {});
$.ajax({
url: '/logon',
type:'post',
data:JSON.stringify({"username": username,"password": password,"remember-me":remeberMe}),
contentType: "application/json",
dataType: "json",
beforeSend: function(request) {
request.setRequestHeader("X-XSRF-TOKEN",token["XSRF-TOKEN"]);
},
success: function(response){
token2 = response["token"];
sessionStorage.setItem("token2",token2);
},
error: function(response){
alert(response)
}
})
})
$("#sub2").click(function(){
var token = document.cookie.split('; ').reduce(function(result, v, i, a) { var k = v.split('='); result[k[0]] = k[1]; return result; }, {});
$.ajax({
url: '/main',
type:'post',
beforeSend: function(request) {
request.setRequestHeader("X-XSRF-TOKEN",token["XSRF-TOKEN"]);
request.setRequestHeader("TOKEN",sessionStorage.getItem("token2"));
},
success: function(response){
alert(response)
},
error: function(response){
alert(response)
}
})
})
})
</script>
</head>
<body>
username: <input type="text" name="username" id="username"></br>
password: <input type="text" name="password" id="password"></br>
remeber-me: <input type="checkbox" name="remember-me" id="remember-me" value="true"></br>
<button style="width: 50px;height: 25px" id="sub">submit</button>
<button style="width: 50px;height: 25px" id="sub2">submit2</button>
</body>
</html>

浙公网安备 33010602011771号