首先要了解一下sercurity的工作原理
建议拜读一下大佬文章
Spring Security 工作原理概览
借用一下大佬的图
首先明确一下我们要实现的功能
- 第一次账号密码成功认证登录后,服务器要向客户端的响应头里要设置我们生成的token,下发token
- 客户端在第一次登录后访问其他api接口时,需要在请求头里带上服务器下发的token才能允许访问接口
我们先来实现第一个功能
首先肯定是要导入jar包了
<!--安全验证-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--jwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.22</version>
</dependency>
hutool是一个工具类,不想用也可以没必要
说明引入security的jar包后再启动项目默认会有个登录页面
账号默认是user,密码会在控制台输出
可以自己在yml配置文件中设置
spring:
security: #httpBasic()认证用户名和密码
user:
name: wjc
password: 123456
这样用户名就改成了wjc 密码也固定为123456了
首先我们要实现一个security的UserDetails
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {
private UserInfo userInfo;
/**
* 返回该账号下的所有权限信息
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<>();
return authorities;
}
@Override
public String getPassword() {
return userInfo.getPassword();
}
@Override
public String getUsername() {
return userInfo.getUserName();
}
/**
* 账号没有过期(默认返回true)
* @return
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 账号没有被锁定(默认返回true)
* @return
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 密码没有过期(默认返回true)
* @return
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 账号可用(默认返回true)
* @return
*/
@Override
public boolean isEnabled() {
return true;
}
}
我们先不考虑角色权限这块先就单纯实现登录块功能
这个userinfo是你自定义的用户实体类,我的是这样的
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel("用户主题类")
public class UserInfo {
@ApiModelProperty("用户id")
private Integer id;
@ApiModelProperty("用户名用来登录")
private String userName;
@ApiModelProperty("昵称")
private String nickName;
@ApiModelProperty("真实姓名")
private String realName;
@ApiModelProperty("手机号码")
private String phone;
@ApiModelProperty("密码")
private String password;
/**
* 1:启用,2禁用
*/
@ApiModelProperty("状态 1:启用 2:禁用")
private Integer status;
@ApiModelProperty("创建时间")
private Date createTime;
@ApiModelProperty("更新时间")
private Date updateTime;
}
这些注解是Lombok和swagger的无足轻重
我们还要实现security的UserDetailsService
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserInfoService userInfoService;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
//通过用户名查找用户
UserInfo user = userInfoService.findByUserName(userName);
if (user==null){
//用户为空的话直接返回空值
return null;
}
//将查找的用户封装到LoginUser类中
return new LoginUser(user);
}
}
UserInfoService就是user实体类的service层这里就是通过用户名从数据库里查询到用户的数据然后封装到我们实现的UserDetails接口LoginUser里去
做到这里其实我们已经可以成功完成登录功能了,但我们还无法生成token,所以我们需要一个TokenUtils
不理解jwt的可以去官网看看
@Component
@Lazy
@Data
public class TokenUtils implements Serializable {
@Value("${token.header}")
private String header;
@Value("${token.secret}")
private String secret;
@Value("${token.expireTime}")
private int expireTime;
/**
* 获取Token
* @param claims
* @return
*/
public String createToken(Map<String,Object>claims){
//设置token过期时间(毫秒数)
long now = System.currentTimeMillis()+(expireTime * 60 * 1000);
return Jwts.builder()
//签发者
.setIssuer("wjc")
.addClaims(claims)
//设置过期时间
.setExpiration(new Date(now))
.signWith(SignatureAlgorithm.HS256,secret).compact();
}
/**
* 解析token
* @param token
* @return
*/
public Claims parseToken(String token){
JwtParser jwtParser = Jwts.parser().setSigningKey(secret);
Jws<Claims> claimsJws = jwtParser.parseClaimsJws(token);
Claims body = claimsJws.getBody();
return body;
}
}
上面的三个属性是从yml配置文件动态注入的,方便以后token生成的更改,这个token工具类不是很完善,网上还有很多根据自己的需求选择即可
#token配置
token:
header: Authorization #令牌自定义标识
secret: wjc-key #令牌密钥
expireTime: 30 #令牌有效期 默认30分钟
好了接下来就是重点的security配置类了
这里我们重点要写代码的就是重写两个处理器
登录成功处理器:successHandler
登录失败处理器:failureHandler
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启注解标注 哪些方法需要鉴权 @PreAuthorize("hasRole('admin')") @PreAuthorize("hasAuthority('sys:user:save')")
public class SecurityConfig extends WebSecurityConfigurerAdapter{
//实现了UserDetailsService的类
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
private UserInfoService userInfoService;
@Autowired
private TokenUtils tokenUtils;
//这里是定义了放行的路径
private static final String[] URL_WHITELISTS ={
"/login/**",
"/swagger-ui.html",
"/swagger-resources/**",
"/webjars/**",
"/v2/**",
"/api/**"
};
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder(){
//这里是可以更改密码加密的方式,security自带的BCryptPassword加密这个安全性更高
也可以根据自己的需求更改
@Override
public String encode(CharSequence rawPassword) {
return super.encode(rawPassword);
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return super.matches(rawPassword, encodedPassword);
}
};
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
//springsecurity通过userDetailsService的loadUserByUsername方法
//去数据库里查询用户并认证
auth.userDetailsService(userDetailsService)
//设置密码加密方式,默认为BCryptPasswordEncoder,也是springsecurity默认的密码加密方式
//这个必须要
.passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable() //这里是解决跨域问题和禁用csrf
.authorizeRequests()
//白名单放行
.antMatchers(URL_WHITELISTS).permitAll()
//其他所有请求都要认证
.anyRequest().authenticated()
//连接
.and()
//关闭session
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//登录
.formLogin()
//登录成功处理
.successHandler(successHandler())
//登录失败处理
.failureHandler(failureHandler());
}
/**
*登录成功处理器
* @return
*/
public AuthenticationSuccessHandler successHandler(){
return new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.setCharacterEncoding("utf-8");
httpServletResponse.setContentType("application/json;charset=utf-8");
//取出此时登录的用户名
String userName = authentication.getName();
UserInfo userInfo = userInfoService.findByUserName(userName);
Map<String,Object> claims = new HashMap<>();
//我们将用户名存到token当中去
claims.put("username",userInfo.getUserName() );
//生成token
String token = tokenUtils.createToken(claims);
httpServletResponse.addHeader(LoginUtil.AUTH,token);
PrintWriter writer = httpServletResponse.getWriter();
//将token包装到同一的返回结果类返回
writer.println(JsonResult.success(userInfo));
//刷新确保成功响应
writer.flush();
writer.close();
}
};
}
/**
*登录失败处理器
* @return
*/
public AuthenticationFailureHandler failureHandler(){
return new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setCharacterEncoding("utf-8");
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter writer = httpServletResponse.getWriter();
writer.println(JsonResult.failure("登录失败"));
writer.flush();
writer.close();
}
};
}
}
上面的JsonResult是同意封装的jsno返回类
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
@ApiModel(value = "通用公共返回类")
public class JsonResult <T> implements Serializable {
public static final int CODE_SUCCESS = 200;
public static final int CODE_FAILURED = 500;
public static final String[] NOOP = new String[] {};
@ApiModelProperty(value = "返回状态")
private int code; // 处理状态:0: 成功
@ApiModelProperty(value = "返回消息")
private String message;
private T data; // 返回数据
/**
* 有data的正常返回
*
* @param data data内容
* @param <T> data类型
*/
public static <T> JsonResult<T> success(T data) {
return new JsonResult<>(CODE_SUCCESS,"success",data);
}
public static <T> JsonResult<T> success() {
return new JsonResult<>(CODE_SUCCESS,"success",null);
}
public static <T> JsonResult<T> success(Integer code) {
return new JsonResult<>(code,"success",null);
}
public static <T> JsonResult<T> success(Integer code,String message) {
return new JsonResult<>(code,message,null);
}
public static <T>JsonResult<T> success(Integer code, String message,T data) {
return new JsonResult<>(code,message,data);
}
public static <T> JsonResult<T> success(Integer code,T data) {
return new JsonResult<>(code,"success",data);
}
public static JsonResult failure(String message) {
return new JsonResult(CODE_FAILURED,message,null);
}
public static <T> JsonResult<T> failure(Integer code,String message,T data) {
return new JsonResult(code,message,data);
}
}
常量定义在这了
public class LoginUtil {
//token的名称
public static String AUTH = "Authorization";
//token前缀
public static String TOKEN_PREFIX = "Bearer ";
}
这个时候我们已经可以成功完成登录操作并下发token
接下来就要实现第二个功能解析token并且放行
@Slf4j
public class JwtAuthenticationFilter extends BasicAuthenticationFilter implements LoginUtil {
@Autowired
private TokenUtils tokenUtils;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
//配置的自定义标识 Authorization
String token = request.getHeader(AUTH);
//Bearer (有个空格)标识
if (StrUtil.isNotBlank(token) && token.startsWith(TOKEN_PREFIX)) {
//生成的token中带有Bearer 标识,去掉标识后就剩纯粹的token了。
String substring = token.substring(TOKEN_PREFIX.length());
try {
//解析token拿到我们生成token的时候存进去的username
Claims claims = tokenUtils.parseToken(substring);
String userName = (String) claims.get("username");
if (StrUtil.isNotBlank(userName)) {
//将查询到的用户信息取其账号(登录凭证)以及密码去生成一个Authentication对象
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userName, null,null);
//将Authentication对象放进springsecurity上下文中(进行认证操作)
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}catch (Exception e){
//这里在解析错误token或token过期时会报异常
log.error("解析token异常"+e.getMessage());
}
}
//走下一条过滤器
chain.doFilter(request,response);
}
}
提一嘴这个**Bearer **(后面带空格的) 标识,就是前端在设置token要加这个前缀这个官网有提到,是规范,不过好像很多前端都不会带这个如果没有就把前面那个去前缀的去掉就可以了。这里try catch了一下是因为当token过期或者token错误时会有异常
然后我们要把这个过滤器加到security过滤链中去
还可以重写一下我们的认证处理器,对为加正确token的访问进行处理
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启注解标注 哪些方法需要鉴权 @PreAuthorize("hasRole('admin')") @PreAuthorize("hasAuthority('sys:user:save')")
public class SecurityConfig extends WebSecurityConfigurerAdapter{
//实现了UserDetailsService的类
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
private UserInfoService userInfoService;
@Autowired
private TokenUtils tokenUtils;
private static final String[] URL_WHITELISTS ={
"/login/**",
"/swagger-ui.html",
"/swagger-resources/**",
"/webjars/**",
"/v2/**",
"/api/**"
};
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager());
return jwtAuthenticationFilter;
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder(){
@Override
public String encode(CharSequence rawPassword) {
return super.encode(rawPassword);
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return super.matches(rawPassword, encodedPassword);
}
};
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
//springsecurity通过userDetailsService的loadUserByUsername方法
//去数据库里查询用户并认证
auth.userDetailsService(userDetailsService)
//设置密码加密方式,默认为BCryptPasswordEncoder,也是springsecurity默认的密码加密方式
//这个必须要
.passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.authorizeRequests()
//白名单放行
.antMatchers(URL_WHITELISTS).permitAll()
//其他所有请求都要认证
.anyRequest().authenticated()
//连接
.and()
//关闭session
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//登录
.formLogin()
//登录成功处理
.successHandler(successHandler())
//登录失败处理
.failureHandler(failureHandler())
.and()
//设置访问被拒绝后的事件(用来处理权限不足时的返回)
.exceptionHandling()
//未登录访问资源
.authenticationEntryPoint(AuthenticationEntryPoint())
//将我们自定义的token过滤器按照一定顺序加入过滤器链
.and()
.addFilter(jwtAuthenticationFilter());
}
/**
*登录成功处理器
* @return
*/
public AuthenticationSuccessHandler successHandler(){
return new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.setCharacterEncoding("utf-8");
httpServletResponse.setContentType("application/json;charset=utf-8");
//取出此时登录的用户名
String userName = authentication.getName();
UserInfo userInfo = userInfoService.findByUserName(userName);
Map<String,Object> claims = new HashMap<>();
claims.put("username",userInfo.getUserName() );
//生成token
String token = tokenUtils.createToken(claims);
httpServletResponse.addHeader(LoginUtil.AUTH,token);
PrintWriter writer = httpServletResponse.getWriter();
//将token包装到同一的返回结果类返回
writer.println(JsonResult.success(userInfo));
//刷新确保成功响应
writer.flush();
writer.close();
}
};
}
/**
*登录失败处理器
* @return
*/
public AuthenticationFailureHandler failureHandler(){
return new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setCharacterEncoding("utf-8");
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter writer = httpServletResponse.getWriter();
writer.println(JsonResult.failure("登录失败"));
writer.flush();
writer.close();
}
};
}
/**
*认证处理器
* @return
*/
public AuthenticationEntryPoint AuthenticationEntryPoint(){
return new AuthenticationEntryPoint(){
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
//前后端分离项目 /login 可以是返回一个json字符串
httpServletResponse.setCharacterEncoding("utf-8");
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter writer = httpServletResponse.getWriter();
writer.println(JsonResult.failure("未登录!或登录失效请重新登录!"));
writer.flush();
writer.close();
}
};
}
}
至此我们的两个功能都可以简单实现了