1.装箱即用的spring security
加入依赖,加个配置即可——应用场景:eureka server,hystrix dashboard,springboot admin等,用于增加登录验证(基于cookie:jssessionId实现用户session的,不能用postman等工具来测试)。该应用场景效果基本nginx配置账号密码验证差不多
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
spring:
security:
user:
name: jwolf
password: 123456
2.追加几个内存用户并配置其访问权限
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
@EnableWebSecurity
public class MemoryUserSecurityConfig extends WebSecurityConfigurerAdapter {
//用户认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//内存里面初始化几个用户
auth.inMemoryAuthentication().passwordEncoder(new MyPasswordEncoder())
//添加用户,密码,角色
.withUser("zs").password("123456").roles("AAA")
//链式编程
.and()
.withUser("ls").password("123456").roles("BBB")
.and()
.withUser("ww").password("123456").roles("CCC", "primary").authorities("PPP");//PPP权限
}
//用户授权 ant风格的path
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/").permitAll() //应用首页所以用户都可以访问,完全无限制
.antMatchers("/test-user/addUs*").hasRole("BBB") // 允许 AAA 角色访问
.antMatchers("/test-user/deleteUser/**").hasAnyAuthority("PPP") //允许有"PPP"权限的访问
.antMatchers("/test-user/updateUser").hasAnyRole("AAA", "BBB", "CCC")
.antMatchers("/test-user/findAllUsers").permitAll()
.anyRequest().authenticated() //其它path都有登录才能访问
.and()
.formLogin();//指定支持基于表单的身份验证,会暴露出/login /logout等端点,默认登录成功调向登录前的,可以自定义登录页面登录成功和失败重定向的url,例如.formLogin().loginPage("/myLogin").successForwardUrl("/xxxx").failureForwardUrl("/yyy")
}
private class MyPasswordEncoder implements org.springframework.security.crypto.password.PasswordEncoder {
@Override
public String encode(CharSequence charSequence) {
return charSequence.toString(); //用户输入密码处理
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return s.equals(charSequence.toString());//与security上下文存储的用户密码比对
}
}
}
@RestController
@RequestMapping("/test-user")
public class TestUserController {
@RequestMapping("/addUser")
String addUser() {
return "这是添加用户!!!";
}
@RequestMapping("/deleteUser")
String deleteUser() {
return "这是删除用户!!!";
}
@RequestMapping("/updateUser")
String updateUser() {
return "这是修改用户!!!";
}
@RequestMapping("/findAllUsers")
String findAllUsers() {
return "这是查询用户!!!";
}
}
3.从DB获取用户信息进行权限控制
配置类主要注入bean UserDetailsService
@Configuration
public class DBUserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
//这里从数据库中读取数据到spring security上下文
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(getPasswordEncoder());
}
//用户授权 ant风格的path
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/").permitAll() //应用首页所以用户都可以访问,完全无限制
.antMatchers("/test-user/addUs*").hasRole("BBB") // 允许 AAA 角色访问
.antMatchers("/test-user/deleteUser/**").hasAnyAuthority("PPP") //允许有"PPP"权限的访问
.antMatchers("/test-user/updateUser").hasAnyRole("AAA", "BBB", "CCC")
.antMatchers("/test-user/findAllUsers").permitAll()
.anyRequest().authenticated() //其它path都有登录才能访问
.and()
.formLogin();//指定支持基于表单的身份验证,会暴露出/login /logout等端点
}
@Bean
public PasswordEncoder getPasswordEncoder(){
return new PasswordEncoder() {
@Override
public String encode(CharSequence password) {
return password.toString();
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
String encodeStr = encode(rawPassword);
return encodedPassword.equalsIgnoreCase(encodeStr);
}
};
}
}
需要实现spring security的 UserDetailsService 接口,下面基本就是常规的业务类型代码了,这里注入的UserMapper
@Service
public class SecurityUserServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SecurityUser user = userMapper.selectByUsername(username);
if (user == null) {
throw new BadCredentialsException("用户名或密码错误");
}
return user;
}
}
entity是这样的,需要实现spring secury的UserDetails
import lombok.Getter;
import lombok.Setter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
@Getter
@Setter
public class SecurityUser implements UserDetails {
private static final long serialVersionUID=1L;
private long id;
private String password;
private String username;
private List<GrantedAuthority> anthorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.anthorities;
}
//账户是否未过期
@Override
public boolean isAccountNonExpired() {
return true;
}
//账户是否未锁定
@Override
public boolean isAccountNonLocked() {
return true;
}
//帐户密码是否未过期,一般有的密码要求性高的系统会使用到,比较每隔一段时间就要求用户重置密码
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//账户是否可用
@Override
public boolean isEnabled() {
return true;
}
}
userMapper.xml是这样的
<resultMap id="userResultMap" type="com.construn.vehicle.user.entity.SecurityUser">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="password" property="password"/>
<collection property="anthorities" ofType="org.springframework.security.core.authority.SimpleGrantedAuthority">
<result column="rolename" property="role"/>
</collection>
</resultMap>
<select id="selectByUsername" resultMap="userResultMap">
select a.id,a.username,a.password,b.rolename from t_user a left join t_role b on a.role_id=b.id
where a.username=#{username}
</select>
4.关于role 与authority区别及权限注解的开启
使用权限注解需要启动类开启,该注解有几个参数,默认都是关的@EnableGlobalMethodSecurity(prePostEnabled = true),@DenyAll @RolesAllowed({"USER", "ADMIN"}) @PermitAll 等开关需要通过jsr250Enabled = true来开启@EnableGlobalMethodSecurity(prePostEnabled = true,jsr250Enabled = true)
核心配置类DBUserSecurityConfig的.antMatchers("/test-user/deleteUser/**").hasAnyAuthority("PPP") 改为权限配置应该是接口使用注解@PreAuthorize("hasAnyAuthority('PPP')"),如果数据存的rolename为ROLE_XXX,或存入SimpleGrantedAuthority的rolename有ROLE_前缀,就应该使用@PreAuthorize("hasAnyRole('PPP')"),role与authority区别参考https://www.cnblogs.com/Rocky_/p/11799772.html
5.整合JWT
增加jwt依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
原生的security认证信息通过cookie-session保存会话session,用户客户端或服务端重启都会导致重新登录,多数情况需要整合jwt,这里较step3多了两个filter,一个用于登录成功将jwt token写到浏览器,一个用于访问资源时解析jwt token放入到security 认证上下文,免密加密用的security亲生的BCryptPasswordEncoder
import org.apache.commons.codec.digest.Md5Crypt;
import org.apache.ibatis.javassist.bytecode.ByteArray;
import org.apache.tomcat.util.security.MD5Encoder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import sun.security.provider.MD5;
import java.util.ArrayList;
import java.util.Collection;
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(getPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/").permitAll() //应用首页所以用户都可以访问,完全无限制
.antMatchers("/test-user/addUs*").hasRole("BBB") // 允许 AAA 角色访问
//.antMatchers("/test-user/deleteUser/**").hasAnyAuthority("PPP") //允许有"PPP"权限的访问
.antMatchers("/test-user/updateUser").hasAnyRole("AAA", "BBB", "CCC")
.antMatchers("/test-user/findAllUsers").permitAll()
.anyRequest().authenticated() //其它path都有登录才能访问
.and().csrf().disable()
.formLogin()//指定支持基于表单的身份验证,会暴露出/login /logout等端点
.and()
.addFilter(new JWTLoginFilter(authenticationManager()))
.addFilter(new JWTAuthenticationFilter(authenticationManager()));
}
@Bean
public PasswordEncoder getPasswordEncoder() {
return new PasswordEncoder() {
@Override
public String encode(CharSequence password) {
//用户注册调用该方法进行密码加密
// BCryptPasswordEncoder对相同字符串每次加密都结果都不一样,但matches()时都能比较成功,具有更高安全性
return new BCryptPasswordEncoder().encode(password);
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
//用户登录将用户表单传入的rawPassword与数据库的密码比对。
return new BCryptPasswordEncoder().matches(rawPassword,encodedPassword);
}
};
}
}
两个过滤器
import com.alibaba.fastjson.JSON;
import com.construn.vehicle.common.base.entity.ResultEntity;
import com.construn.vehicle.user.entity.SecurityUser;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.authentication.AuthenticationManager;
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.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;
import java.util.stream.Collectors;
/**
* 验证用户名密码正确后,生成一个token,并将token返回给客户端
* 该类继承自UsernamePasswordAuthenticationFilter,重写了其中的2个方法 ,
* attemptAuthentication:接收并解析用户凭证。
* successfulAuthentication:用户成功登录后,这个方法会被调用,我们在这个方法里生成token并返回。
*/
public class JWTLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public JWTLoginFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
String username = request.getParameter("username");
String password = request.getParameter("password");
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password, null));
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication auth) {
Claims claims = Jwts.claims();
SecurityUser user = (SecurityUser) auth.getPrincipal();
claims.put("userId", user.getId());
claims.put("role", auth.getAuthorities().stream().map(s -> s.getAuthority()).collect(Collectors.toList()));
String token = Jwts.builder()
.setClaims(claims)
.setSubject(auth.getName())
.setExpiration(new Date(System.currentTimeMillis() + 600 * 1000))
.signWith(SignatureAlgorithm.HS512, "jwolf").compact();
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
ResultEntity result = ResultEntity.success(token, "登录成功");
PrintWriter out;
try {
out = response.getWriter();
out.print(JSON.toJSONString(result));
out.flush();
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
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.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
public class JWTAuthenticationFilter extends BasicAuthenticationFilter {
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
/**
* 从request中获取token并解析,拿到用户信息,放置到SecurityContextHolder,这样便完成了springsecurity和jwt的整合。
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
String token = request.getHeader("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
chain.doFilter(request, response);
return;
}
Claims claims = Jwts.parser().setSigningKey("jwolf").parseClaimsJws(token.replace("Bearer ", ""))
.getBody();
List<String> roles = claims.get("role", List.class);
List<SimpleGrantedAuthority> auth = roles.stream().map(s -> new SimpleGrantedAuthority(s)).collect(Collectors.toList());
//如果controller还需要userId等信息其它更多信息,可以第一个参数传入甚至整个claims,最好不要再次从header里获取token二次解析出用户信息,
//然后在controller直接SecurityContextHolder.getContext().getAuthentication()获取;
UsernamePasswordAuthenticationToken authentication =new UsernamePasswordAuthenticationToken( claims.getSubject(), null, auth);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
}
}
其中登录过滤器可以优化,抽出JWT工具类,并可以不用该过滤器,使用登录成功处理器AuthenticationSuccessHandler进行替换,登录失败及登陆security也提供了类似的接口可以自己实现然后在security核心配置即可如登录成功: .successHandler(userLoginSuccessHandler) 参考https://www.tuicool.com/articles/Q7fU73E