Spring 是一个非常流行和成功的 Java 应用开发框架。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分。用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。
Spring和Token整合,其实是对Spring Security 添加登陆和验证filter,不再以session作为登陆验证的标准,而是每次从请求中取token进行校验,如果token是正确的,解析出用户信息并交给Spring Security进行下一步操作。
Git地址:
品茗IT:提供在线快速构建Spring项目工具。
**如果大家正在寻找一个java的学习环境,或者在开发中遇到困难,可以<a
href="https://jq.qq.com/?_wv=1027&k=52sgH1J"
target="_blank">
加入我们的java学习圈,点击即可加入
</a>
,共同学习,节约学习时间,减少很多在学习中遇到的难题。**
<?xml version="1.0"?> <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>cn.pomit</groupId> <artifactId>SpringWork</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <artifactId>Token</artifactId> <packaging>jar</packaging> <name>Token</name> <url>http://maven.apache.org</url> <dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.0</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-core</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> <dependency> <groupId>cn.pomit</groupId> <artifactId>Mybatis</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency> </dependencies> <build> <finalName>Token</finalName> </build> </project>
父pom管理了所有依赖jar包的版本,地址:
https://www.pomit.cn/spring/SpringWork/pom.xml
Spring整合Security需要配置Security的安全控制策略,首先需要在web.xml中配置Spring Security的filter。
在web.xml中加入这几行即可:
<!-- Spring Security 的过滤配置,表明请求需要经过这个类的过滤和判断 --> <filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
Spring Security的需要一个全局的配置,配置哪些url经过验证,哪些无需验证,验证成功/失败的处理器,全局验证失败处理器,登陆地址,登陆用户名密码字段,自定义的filter等等一堆信息。
这里我们直接用注解的形式去写了,用xml的形式去写感觉有点麻烦,后面我会加个xml,把token的管理器写进去,当然,这个管理器也可以直接用注解,这里先不说。
TokenWebSecurityConfig:
package cn.pomit.springwork.token.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; 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.core.userdetails.UserDetailsService; import cn.pomit.springwork.token.filter.TokenAuthenticationFilter; import cn.pomit.springwork.token.filter.TokenLoginFilter; import cn.pomit.springwork.token.handler.DefaultPasswordEncoder; import cn.pomit.springwork.token.handler.TokenLogoutHandler; import cn.pomit.springwork.token.handler.UnauthorizedEntryPoint; import cn.pomit.springwork.token.manager.TokenManager; @Configuration public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter { private UserDetailsService userDetailsService; private TokenManager tokenManager; private DefaultPasswordEncoder defaultPasswordEncoder; @Autowired public TokenWebSecurityConfig(UserDetailsService userDetailsService, DefaultPasswordEncoder defaultPasswordEncoder, TokenManager tokenManager) { this.userDetailsService = userDetailsService; this.defaultPasswordEncoder = defaultPasswordEncoder; this.tokenManager = tokenManager; } @Override protected void configure(HttpSecurity http) throws Exception { http.exceptionHandling().authenticationEntryPoint(new UnauthorizedEntryPoint()).and().csrf().disable() .authorizeRequests().antMatchers("/login").permitAll().antMatchers("/public/**").permitAll() .anyRequest().authenticated().and().logout().logoutUrl("/logout") .addLogoutHandler(new TokenLogoutHandler(tokenManager)).and() .addFilter(new TokenLoginFilter(authenticationManager(), tokenManager)) .addFilter(new TokenAuthenticationFilter(authenticationManager(), tokenManager)).httpBasic(); } @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoder); } }
这里面的东西需要讲一下:
UserDetailsService是spring的规范,用来查询用户信息做检验用的,需要我们自己实现一个service去查询用户信息,
这个service在TokenWebSecurityConfig中被调用,自动装配到Spring的Authentication中。其实就是拿走了用户名密码,TokenUserDetails是service返回的实体,实现了UserDetails接口,这个接口要求你必须返回实现用户名和密码的获取接口。用户名和密码会在登陆过滤器中和前端传过来的用户名密码做对比。这个过程是隐藏的,如果你想自定义对比过程,那要自己写一个AuthenticationProvider了。
TokenUserDetailsService:
package cn.pomit.springwork.token.service; 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.stereotype.Service; import cn.pomit.springwork.mybatis.domain.UserInfo; import cn.pomit.springwork.mybatis.service.UserInfoService; import cn.pomit.springwork.token.model.TokenUserDetails; @Service public class TokenUserDetailsService implements UserDetailsService { @Autowired private UserInfoService appUserService; @Override public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException { UserInfo user; try { user = appUserService.getUserInfoByUserName(userName); } catch (Exception e) { throw new UsernameNotFoundException("user select fail"); } if (user == null) { throw new UsernameNotFoundException("no user found"); } else { try { return new TokenUserDetails(user); } catch (Exception e) { throw new UsernameNotFoundException("user role select fail"); } } } }
这里用到了UserInfoService ,它是个查询数据库的一个service,UserInfoService是依赖包Mybatis项目中定义的一个数据库访问的service,这里就不写了,可以在快速构建Spring项目工具中查看Mybatis组合组件的代码。
TokenUserDetails:
package cn.pomit.springwork.token.model; import java.util.Collection; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.UserDetails; import cn.pomit.springwork.mybatis.domain.UserInfo; public class TokenUserDetails extends UserInfo implements UserDetails { public TokenUserDetails(UserInfo appUser) { super(appUser); } /** * */ private static final long serialVersionUID = 6272869114201567325L; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return AuthorityUtils.createAuthorityList("USER"); } @Override public String getUsername() { return super.getUserName(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } @Override public String getPassword() { return super.getPasswd(); } }
登录过滤器是对前端传过来的数据解析,并调用attemptAuthentication去验证的过程,其中successfulAuthentication是登录成功的处理方法,unsuccessfulAuthentication是失败的处理方法。
TokenLoginFilter:
package cn.pomit.springwork.token.filter; import com.fasterxml.jackson.databind.ObjectMapper; import cn.pomit.springwork.token.manager.TokenManager; import cn.pomit.springwork.token.model.LoginUserReq; import cn.pomit.springwork.token.model.LoginUserRes; import cn.pomit.springwork.token.model.ResultCode; import cn.pomit.springwork.token.model.ResultModel; import cn.pomit.springwork.token.model.TokenUserDetails; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.ProviderNotFoundException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.ArrayList; public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter { private AuthenticationManager authenticationManager; private TokenManager tokenManager; public TokenLoginFilter(AuthenticationManager authenticationManager, TokenManager tokenManager) { this.authenticationManager = authenticationManager; this.tokenManager = tokenManager; this.setPostOnly(false); this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login")); } @Override public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException { try { LoginUserReq user = new ObjectMapper().readValue(req.getInputStream(), LoginUserReq.class); return authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword(), new ArrayList<>())); } catch (IOException e) { throw new RuntimeException(e); } } @Override protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication auth) throws IOException, ServletException { TokenUserDetails user = (TokenUserDetails) auth.getPrincipal(); String token = tokenManager.createToken(user.getUsername()); LoginUserRes loginUserRes = new LoginUserRes(); loginUserRes.setUsername(user.getUsername()); loginUserRes.setToken(token); ResultModel rm = new ResultModel(ResultCode.CODE_00000); rm.setData(loginUserRes); ObjectMapper mapper = new ObjectMapper(); res.setStatus(HttpStatus.OK.value()); res.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); mapper.writeValue(res.getWriter(), rm); } @Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { response.setStatus(HttpStatus.OK.value()); ObjectMapper mapper = new ObjectMapper(); ResultModel rm = null; if (e instanceof BadCredentialsException) { rm = new ResultModel(ResultCode.CODE_00001.getCode(), e.getMessage()); } else if (e instanceof UsernameNotFoundException) { rm = new ResultModel(ResultCode.CODE_00011); } else if (e instanceof AuthenticationCredentialsNotFoundException) { rm = new ResultModel(ResultCode.CODE_00003); } else if (e instanceof ProviderNotFoundException) { rm = new ResultModel(ResultCode.CODE_10000, e.getMessage()); } else { rm = new ResultModel(ResultCode.CODE_00013); } response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); mapper.writeValue(response.getWriter(), rm); } }
这里面:
验证过滤器的功能很简单,就是从可以拿到token的地方拿到token,然后从tokenManager中解析出用户信息,然后将用户信息塞到Authentication中,这样Spring security就没话说了,通过了。
在这里有个重要的地方要说下:如果session没有被禁用,那Spring security还是自动从cookie里解析出sessionId,如果这个session已经验证了一次,服务器看到有验证信息,那后面带token的解析和不带token的解析都是一样的了。虽然我们带了token,又生成了新的Authentication替换了session中的Authentication,略显多余。token一般用在无法使用session的场景。
TokenAuthenticationFilter:
package cn.pomit.springwork.token.filter; import java.io.IOException; import java.util.ArrayList; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.util.StringUtils; import cn.pomit.springwork.token.manager.TokenManager; public class TokenAuthenticationFilter extends BasicAuthenticationFilter { private TokenManager tokenManager; public TokenAuthenticationFilter(AuthenticationManager authManager, TokenManager tokenManager) { super(authManager); this.tokenManager = tokenManager; } @Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException { String header = req.getHeader("token"); if (header == null) { chain.doFilter(req, res); return; } UsernamePasswordAuthenticationToken authentication = getAuthentication(req); if (authentication != null) { SecurityContextHolder.getContext().setAuthentication(authentication); } chain.doFilter(req, res); } private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) { // token置于header里 String token = request.getHeader("token"); if (token != null && !"".equals(token.trim())) { // parse the token. String userName = tokenManager.getUserFromToken(token); if (!StringUtils.isEmpty(userName)) { return new UsernamePasswordAuthenticationToken(userName, token, new ArrayList<>()); } return null; } return null; } }
token管理器不要求一定用什么框架,我们这里选择了jwt token框架,是为了简单,而且支持多机,而且不需要第三方服务支持。
如果将token存在本地,那token只能单机用了。
如果将token存在redis,那多个机器可以共享token,但是要部署redis,还要连接redis。麻烦。这里不讲。
jwt token 就是太长,又丑又长。
我们先定义个一个接口TokenManager,具体实现可以自己写,下面我会写个jwt 的实现。
TokenManager:
package cn.pomit.springwork.token.manager; public interface TokenManager { public String createToken(String username); public String getUserFromToken(String token); public void removeToken(String token); }
JwtTokenManager :
package cn.pomit.springwork.token.manager; import java.util.Date; import io.jsonwebtoken.CompressionCodecs; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; public class JwtTokenManager implements TokenManager{ private Long tokenExpiration; private String tokenSignKey; @Override public String createToken(String username) { String token = Jwts.builder().setSubject(username) .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration)) .signWith(SignatureAlgorithm.HS512, tokenSignKey).compressWith(CompressionCodecs.GZIP).compact(); return token; } @Override public String getUserFromToken(String token) { String user = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody().getSubject(); return user; } @Override public void removeToken(String token) { //jwttoken无需删除,客户端扔掉即可。 } }
这里模拟啥都不处理。
package cn.pomit.springwork.token.handler; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; @Component public class DefaultPasswordEncoder implements PasswordEncoder { public DefaultPasswordEncoder() { this(-1); } /** * @param strength * the log rounds to use, between 4 and 31 */ public DefaultPasswordEncoder(int strength) { } public String encode(CharSequence rawPassword) { return rawPassword.toString(); } public boolean matches(CharSequence rawPassword, String encodedPassword) { return rawPassword.toString().equals(encodedPassword); } }
登出控制器TokenLogoutHandler:
package cn.pomit.springwork.token.handler; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.logout.LogoutHandler; import com.fasterxml.jackson.databind.ObjectMapper; import cn.pomit.springwork.token.manager.TokenManager; import cn.pomit.springwork.token.model.ResultCode; import cn.pomit.springwork.token.model.ResultModel; public class TokenLogoutHandler implements LogoutHandler { private TokenManager tokenManager; public TokenLogoutHandler(TokenManager tokenManager) { this.tokenManager = tokenManager; } @Override public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { String token = request.getHeader("token"); if (token != null) { tokenManager.removeToken(token); } ResultModel rm = new ResultModel(ResultCode.CODE_00000); ObjectMapper mapper = new ObjectMapper(); response.setStatus(HttpStatus.OK.value()); response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); try { mapper.writeValue(response.getWriter(), rm); } catch (Exception e) { e.printStackTrace(); } } }
未授权处理器UnauthorizedEntryPoint:
package cn.pomit.springwork.token.handler; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.http.MediaType; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import com.fasterxml.jackson.databind.ObjectMapper; import cn.pomit.springwork.token.model.ResultCode; import cn.pomit.springwork.token.model.ResultModel; public class UnauthorizedEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { ResultModel rm = new ResultModel(ResultCode.CODE_00005, "401未授权!"); ObjectMapper mapper = new ObjectMapper(); response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); mapper.writeValue(response.getWriter(), rm); } }
LoginUserReq :
package cn.pomit.springwork.token.model; public class LoginUserReq { String userName; String password; public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }
LoginUserRes :
package cn.pomit.springwork.token.model; public class LoginUserRes { String token; String username; public String getToken() { return token; } public void setToken(String token) { this.token = token; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } }
ResultCode :
package cn.pomit.springwork.token.model; /** * 响应码及其描述 Created by txl on 15/7/9. */ public enum ResultCode { /** * 通用 */ CODE_00000("00000", "操作成功"), CODE_00001("00001", "请求失败"), CODE_00002("00002", "错误的请求方法"), CODE_00003("00003", "非法的参数字段"), CODE_00004("00004", "异常抛出"), CODE_00005("00005", "权限不足"), CODE_00006("00006", "分页limit参数错误"), CODE_00007("00007", "分页offset参数错误"), CODE_00009("00009", "请求过于频繁"), CODE_00010("00010", "数据已存在"), CODE_00011("00011", "数据不存在"), CODE_00012("00012", "参数缺失"), CODE_00013("00013", "系统维护中"), CODE_00014("00014", "token缺失"), CODE_00015("00015", "token失效"), CODE_00016("00016", "签名错误"), CODE_10000("10000", "操作部分成功"), /** * 系统 */ CODE_30000("30000", "系统ID错误"), /** * 授权 */ CODE_40001("40001", "用户未找到"), CODE_40002("40002", "该用户状态异常"), CODE_40003("40003", "该用户已被删除"), CODE_40004("40004", "授权异常"), CODE_99999("99999", "签名无效"); private String code; private String desc; ResultCode(String code, String desc) { this.code = code; this.desc = desc; } public String getCode() { return code; } public String getDesc() { return desc; } /** * 根据code匹配枚举 * * @param code * @return */ public static ResultCode getResultCodeByCode(String code) { for (ResultCode resultCode : ResultCode.values()) { if (code.equals(resultCode.getCode())) { return resultCode; } } return null; } public static ResultCode getResultCodeByDesc(String desc) { for (ResultCode resultCode : ResultCode.values()) { if (desc.equals(resultCode.getDesc())) { return resultCode; } } return null; } }
ResultModel :
package cn.pomit.springwork.token.model; public class ResultModel { private String error_code; private String message; private Object data; public ResultModel(String error_code, String message) { this.error_code = error_code; this.message = message; } public ResultModel(String error_code, String message, Object data) { this.error_code = error_code; this.message = message; this.data = data; } public ResultModel(ResultCode resultCodeEnum, Object data) { this.error_code = resultCodeEnum.getCode(); this.message = resultCodeEnum.getDesc(); this.data = data; } public ResultModel(ResultCode resultCodeEnum) { this.error_code = resultCodeEnum.getCode(); this.message = resultCodeEnum.getDesc(); } public String getError_code() { return error_code; } public void setError_code(String error_code) { this.error_code = error_code; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public Object getData() { return data; } public void setData(Object data) { this.data = data; } }
详细完整代码,可以在Spring组件化构建中选择查看,并下载。
本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。
我来说两句