springboot-security笔记
目录
开始
- 基于springboot笔记搭建项目;
- 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- 编写
securityConfig
权限配置类继承WebSecurityConfigurerAdapter
,下面步骤中配置都在这里面做配置;
用户和密码
用户信息类
public class LoginUser extends User {
@Getter
@Setter
private String detail;
public LoginUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
public LoginUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
}
}
获取用户类
编写获取用户的Bean:UserDetailsService
:
@Autowired
private PasswordEncoder passwordEncoder;
@Bean
public UserDetailsService userDetailsService() {
// todo: 从数据库查询用户和角色信息
// 方式一
UserDetailsService userDetailsService = new UserDetailsService() {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LoginUser user = new LoginUser(username, passwordEncoder.encode("1234"),
AuthorityUtils.commaSeparatedStringToAuthorityList(username));
return user;
}
};
// 方式二
/*JdbcDaoImpl userDetailsService = new JdbcDaoImpl(){
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//自定义查询用户
return super.loadUserByUsername(username);
}
};
// 方式三:自定义查询用户与角色的sql
userDetailsService.setDataSource(dataSource);
// select需要有3个字段:username、password、enable
userDetailsService.setUsersByUsernameQuery("select * from user where username=?");
userDetailsService.setAuthoritiesByUsernameQuery("select username, role from user u, role r, user_role ur where u.id=ur.uid and r.id=ur.rid and uid=?");
*/
return userDetailsService;
}
密码编码器
@Bean
public PasswordEncoder passwordEncoder() {
// 使用spring security自带的
// return new BCryptPasswordEncoder();
// 使用自定义的
// todo: 密码加密加盐
return new PasswordEncoder() {
@Override
public String encode(CharSequence rawPassword) {
return rawPassword.toString();
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
System.out.println("===>用户输入密码:" + rawPassword);
System.out.println("===>数据库密码:" + encodedPassword);
return rawPassword.equals(encodedPassword);
}
};
}
配置
配置用户查询方法与密码匹配器
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
}
忽略的地址
说明:一般用来配置无需权限校验的路径,也可以在HttpSecurity中配置,但是在web.ignoring()中配置效率更高。web.ignoring()是一个忽略的过滤器,而HttpSecurity中定义了一个过滤器链,即使permitAll()放行还是会走所有的过滤器,直到最后一个过滤器FilterSecurityInterceptor认定是可以放行的,才能访问。配置如下:
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers("/static/**");
}
登录失败处理
@Bean
public AuthenticationFailureHandler failureHandler() {
return new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
if (e instanceof UsernameNotFoundException) {
System.out.println(e.getMessage());
request.getRequestDispatcher("/loginPage?msg=用户不存在").forward(request, response);
} else if (e instanceof BadCredentialsException) {
System.out.println(e.getMessage());
request.getRequestDispatcher("/loginPage?msg=用户名或密码错误").forward(request, response);
} else {
System.out.println(e.getMessage());
request.getRequestDispatcher("/loginPage?msg=账号异常").forward(request, response);
}
}
};
}
记住密码
用于将记住密码的信息保存到数据库,不使用该类默认会保存在cookie中
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
//系统在启动的时候生成“记住我”的数据表(只能使用一次)
//tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
自定义资源权限
springsecurity默认只支持配置式的角色资源权限,使用不够灵活,此部分就是用于自定义角色资源权限的,如不需要,可以不配置。
权限数据源
- 新建资源权限数据源类,用于加载资源权限;
@Component
public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
/**
* 资源权限, key为url,value为可以访问该url的角色列表
*/
private volatile LinkedHashMap<String, Collection<ConfigAttribute>> urlPermMap = new LinkedHashMap<>();
private AntPathMatcher antPathMatcher = new AntPathMatcher();
public void setUrlPermMap(){
// todo:从数据库获取
Collection<ConfigAttribute> perm1 = new HashSet<>();
perm1.add(() -> "admin");
urlPermMap.put("/admin", perm1);
Collection<ConfigAttribute> perm2 = new HashSet<>();
perm2.add(() -> "user");
urlPermMap.put("/user", perm2);
Collection<ConfigAttribute> perm3 = new HashSet<>();
perm3.add(() -> "user");
perm3.add(() -> "admin");
urlPermMap.put("/info", perm3);
}
public MySecurityMetadataSource(){
// 初始化资源权限
setUrlPermMap();
}
// 凡是被springSecurity拦截的请求都会执行该方法
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
FilterInvocation fi = (FilterInvocation) o;
String url = fi.getRequestUrl();
// 资源权限为空,初始化资源
for (String urlPerm : urlPermMap.keySet()) {
if(antPathMatcher.match(urlPerm, url)){
// 根据url返回角色集合
return urlPermMap.get(urlPerm);
}
}
// 这个url没有配权限
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
决策管理器
自定义决策管理器,判断是否有访问权限;
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
// 没有角色集合时,放行
if (collection == null || collection.isEmpty()) {
return;
}
// 当前登录的用户包含当前url的角色时放行
for (GrantedAuthority currRole : authentication.getAuthorities()) {
for (ConfigAttribute urlRole : collection) {
if(currRole.getAuthority().equals(urlRole.getAttribute())){
// 拥有权限,放行
return;
}
}
}
throw new AccessDeniedException("没有权限");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
http配置
配置HttpSecurity
:
@Autowired
private MyAccessDecisionManager myAccessDecisionManager;
@Autowired
private MySecurityMetadataSource mySecurityMetadataSource;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private AuthenticationFailureHandler failureHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/myLogin", "/loginNoPass").permitAll()
.anyRequest().authenticated()
.and().formLogin().loginPage("/loginPage").loginProcessingUrl("/login")
.defaultSuccessUrl("/").failureHandler(failureHandler)//.failureForwardUrl("/loginPage")
.permitAll()
.usernameParameter("user").passwordParameter("pass")
.and().logout().logoutSuccessUrl("/loginPage")//.logoutSuccessHandler(null)
.and().exceptionHandling().accessDeniedPage("/deny")//.accessDeniedHandler(null)
// 使用userDetailsService用Token从数据库中获取用户自动登录
.and().rememberMe().tokenValiditySeconds(1800).key("token_key").userDetailsService(userDetailsService)
.and().csrf().disable();
// .exceptionHandling().authenticationEntryPoint((req, res, auth) -> {res.sendRedirect("/loginPage");});
// 使用自定义url权限,则上面配置的url权限会失效
FilterSecurityInterceptor filterSecurityInterceptor = new FilterSecurityInterceptor();
filterSecurityInterceptor.setSecurityMetadataSource(mySecurityMetadataSource);
filterSecurityInterceptor.setAccessDecisionManager(myAccessDecisionManager);
http.addFilterBefore(filterSecurityInterceptor, FilterSecurityInterceptor.class);
}
上面的配置依次为
- 无需授权就可以访问的url
- 其他所有url都需要授权验证
- 登录页url,登录url
- 登录成功/失败时的跳转地址(处理方法)
- 登录的用户名、密码字段
- 退出成功的跳转地址(处理方法)
- 没有权限时的跳转地址(处理方法)
- 记住密码功能(默认保存在cookie,可以通过tokenRepository配置为保存在数据库----这对表有固定格式要求)
- 关闭csrf
- 未登录访问的处理方法,配置了该项,则第3项的登录页url跳转功能将失效;
- 最后4行的配置用于自定义url资源权限,即动态实现角色和url的拦截,如不需要,可以删除;
附:代码中注释掉的处理方法,主要是针对前后端分离的项目,在前后端分离的项目中,需要将跳转地址改为自定义处理方法,用于返回权限json。
到此,登录已经可以使用了,其中/loginPage
为自定义的登录页面,/login
为登录接口,接收user
,pass
,remember-me
三个参数。
自定义登录
配置
要使用自定义登录,需要先有一个认证管理器;
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
自定义登录
除了访问内置的登录接口进行登录,也可以使用下面的方法进行登录:
@Autowired
private AuthenticationManager authenticationManager;
@RequestMapping(value = "/myLogin")
public String login(String user, String pass, HttpServletRequest request, HttpServletResponse response){
System.out.println(request.getMethod());
if(user == null || pass == null){
return "login";
}
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user, pass);
try{
//使用SpringSecurity拦截登陆请求 进行认证和授权
Authentication authenticate = authenticationManager.authenticate(token);
SecurityContextHolder.getContext().setAuthentication(authenticate);
request.getSession().setAttribute("SPRING_SECURITY_CONTEXT", SecurityContextHolder.getContext());
//记住密码功能(还需要在请求中添加参数:remember-me=true)
RememberMeServices rememberMeServices = new TokenBasedRememberMeServices("token_key", userDetailsService);
rememberMeServices.loginSuccess(request, response, authenticate);
}catch (Exception e){
e.printStackTrace();
return "login";
}
return "redirect:/";
}
免密登录
某些情况下,我们可能不需要账号密码进行登录和授权,这个方法就可以做到;
@RequestMapping("/loginNoPass")
public String loginNoPass(String username, HttpServletRequest request){
//Arrays.asList(SimpleGrantedAuthority())
User user = new User(username, "1", AuthorityUtils.createAuthorityList("admin"));
// 使用这个token,则不会在进行认证和授权,需要在这里完成授权
PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken(user, user.getPassword(), user.getAuthorities());
token.setDetails(new WebAuthenticationDetails(request));
SecurityContextHolder.getContext().setAuthentication(token);
request.getSession().setAttribute("SPRING_SECURITY_CONTEXT", SecurityContextHolder.getContext());
return "redirect:/";
}
用户信息
用户信息一般可以在SecurityContextHolder
中获取
@RequestMapping("/info")
public String info(Model model, HttpServletRequest request){
// request.getUserPrincipal()与SecurityContextHolder.getContext()返回值相同
// 第一行能获取到LoginUser, (这里强转为UsernamePasswordAuthenticationToken的父类,避免不同登录方式会报错的问题)
model.addAttribute("data", "loginUser: ===>" + ((AbstractAuthenticationToken) request.getUserPrincipal()).getPrincipal() +
"\nremoteUser: ===>" + request.getRemoteUser() +
"\ncontext: ===>" + SecurityContextHolder.getContext().getAuthentication().getName());
return "index";
}
redis
springsecurity使用的session来管理认证信息,要整合redis只需要使用redis session即可,步骤如下:
- 依赖
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
- 配置
# 使用redis session
spring.session.store-type=redis
- 启动类添加注解
@EnableRedisHttpSession
此时,不仅仅springsecurity使用了redis,就是我们代码中使用的普通session也会存入redis,此时,就可以通过nginx进行负载均衡了。