源码下载
由于SpringSecurity是一个安全框架,所以SpringSecurity就会有认证(用户登录是否成功)和授权(认证成功后的用户有那些权限)的功能。所以接下来连接数据库来看看是如何使用。
一、数据库表(RBAC)
- 用户表(t_user) t_user.png
- 角色表(t_role) t_role.png
- 权限表(t_menu) t_menu.png
- 用户角色中间表(t_user_role) t_user_role.png
- 角色权限中间表(t_role_menu) t_role_menu.png
二、自定义用户名和密码
上一节体验一下SpringSecurity,但是登陆的用户名和密码都是固定的,显然是不符合我们真实开发。
1、UserDetails是SpringSecurity用户信息接口
@Setter
public class DemoUserDetails implements UserDetails {
private String username;
private String password;
private int enabled;
private Collection<? extends GrantedAuthority> authorities;
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
public String getPassword() {
return this.password;
}
public String getUsername() {
return this.username;
}
public boolean isAccountNonExpired() {
return true;
}
public boolean isAccountNonLocked() {
return true;
}
public boolean isCredentialsNonExpired() {
return true;
}
public boolean isEnabled() {
return this.enabled == 1;
}
}
2、UserDetailsService是SpringSecurity认证用户并且返回用户信息接口
@Component
public class DemoUserDetailsServiceImpl implements UserDetailsService {
@Autowired
private IDemoUserMapper userMapper;
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 通过用户名获取用户信息
DemoUserDetails userInfo = this.userMapper.fetchUserInfoByUserName(username);
if (userInfo == null) {
throw new RuntimeException("用户不存在");
}
// 通过用户名获取用户角色资源
List<String> roleCodes = this.userMapper.fetchRoleCodesByUserName(username);
// 通过用户名获取用户权限
List<String> authorties = this.userMapper.fetchMenuUrlsByUserName(username);
// 角色也是一种用户权限
if (authorties != null && roleCodes != null) {
authorties.addAll(roleCodes);
}
userInfo.setAuthorities(
AuthorityUtils.commaSeparatedStringToAuthorityList(
String.join(",", authorties)
)
);
return userInfo;
}
}
- 3、mapper处理
@Mapper
public interface IDemoUserMapper {
/**
* @param username 用户名称
* @return 用户信息
*/
@Select("SELECT * FROM t_user WHERE username = #{username}")
DemoUserDetails fetchUserInfoByUserName(@Param("username") String username);
/**
* @param username 用户名称
* @return 角色id列表
*/
@Select("SELECT ur.role_id \n" +
"FROM t_user_role ur \n" +
"LEFT JOIN t_user u\n" +
"ON ur.user_id = u.id\n" +
"WHERE u.username = #{username}")
List<String> fetchRoleIdsByUserName(@Param("username") String username);
/**
* @param username 用户名称
* @return 角色code列表
*/
@Select("SELECT r.role_code\n" +
"FROM t_role r\n" +
"LEFT JOIN (t_user_role ur \n" +
"LEFT JOIN t_user u\n" +
"ON ur.user_id = u.id)\n" +
"ON r.id = ur.role_id\n" +
"WHERE u.username = #{username}")
List<String> fetchRoleCodesByUserName(@Param("username") String username);
/**
* @param username 用户名称
* @return 权限url列表
*/
@Select("SELECT m.url\n" +
"FROM t_menu m\n" +
"LEFT JOIN (t_role_menu rm\n" +
"LEFT JOIN (t_user_role ur \n" +
"LEFT JOIN t_user u\n" +
"ON ur.user_id = u.id)\n" +
"ON rm.role_id = ur.role_id)\n" +
"ON m.id = rm.menu_id\n" +
"WHERE u.username = #{username}")
List<String> fetchMenuUrlsByUserName(@Param("username") String username);
/**
* @param roleIds 角色id列表
* @return 权限url列表
*/
@Select({
"<script>",
"SELECT m.url\n" +
"FROM t_menu m\n" +
"LEFT JOIN t_role_menu rm\n" +
"ON m.id = rm.menu_id\n" +
"WHERE rm.role_id IN ",
"<foreach collection='roleIds' item='roleId' open='(' separator=',' close=')'>",
"#{roleId}",
"</foreach>",
"</script>"
})
List<String> fetchMenuUrlsByRoleIds(@Param("roleIds") List<String> roleIds);
}
- 至此,在用户登录时,用户名和密码必须要输入我们数据库中存在的用户才能正确认证通过,但是登陆页面依然是使用的默认页面,所以需要自定义登录页
三、自定义登录页
- 1、自定义登录界面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>首页</title>
<script src="https://cdn.staticfile.org/jquery/1.12.3/jquery.min.js"></script>
</head>
<body>
<form action="/authentication/form" method="post">
<span>用户名称</span><input type="text" name="username"/> <br>
<span>用户密码</span><input type="password" name="password"/> <br>
<input type="submit" value="登陆">
</form>
</body>
</html>
- BrowserSecurityConfig配置自定义登录页
/**
* Web端security配置
*/
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// http.httpBasic() // 配置弹窗登录界面
http.formLogin()
.loginPage("/bw-login.html")
.and()
.authorizeRequests()
.anyRequest()
.authenticated();
}
}
自定义登录.png
- 错误原因
- 用户未认证请求下请求资源时,会调转到自定义登录也,但是在跳转自定义登录页的路径也是需要授权才能访问的资源,所以需要将登录页资源放行,用户不用去认证就可以访问
/**
* Web端security配置
*/
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.loginPage("/bw-login.html") // 自定义登录界面
.loginProcessingUrl("/authentication/form") // 默认处理的/login,自定义登录界面需要指定请求路径
.and()
.authorizeRequests()
.antMatchers("/bw-login.html").permitAll()
.anyRequest()
.authenticated();
}
}
- 再次登录时,发现输入正确的用户名和密码,但是不会调转到访问资源的路径,这是因为缺少csrf().disable()配置,SpringSecurity默认是防护csrf攻击的,所以会造成跳转失败
/**
* Web端security配置
*/
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.formLogin()
.loginPage("/bw-login.html") // 自定义登录界面
.loginProcessingUrl("/authentication/form") // 默认处理的/login,自定义登录界面需要指定请求路径
.and()
.authorizeRequests()
.antMatchers("/bw-login.html").permitAll()
.anyRequest()
.authenticated();
}
}
- UsernamePasswordAuthenticationFilter 默认情况下会拦截"/login"路径,POST方法,但是在我们自定义的表单登录中action为"/authentication/form",这样UsernamePasswordAuthenticationFilter无法拦截,所以需要配置loginProcessingUrl来告诉过滤器需要拦截自定义的action
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
- 2、现在可以通过配置使用自己的登录页,为了更加灵活,应该做到不同类型的请求,返回不同类型的响应 处理不同类型请求.png
-
2.1 BrowserSecurityController
/**
* 当需要身份认证时,跳转到这里
*/
@RestController
public class BrowserSecurityController {
private Logger logger = LoggerFactory.getLogger(getClass());
// 获取session缓存
private RequestCache requestCache = new HttpSessionRequestCache();
// 跳转工具类
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
/**
* 当需要身份认证时,跳转到这里
*
* @param request
* @param response
* @return
* @throws IOException
*/
@RequestMapping("/authentication/require")
@ResponseStatus(code = HttpStatus.UNAUTHORIZED)
public R requireAuthentication(HttpServletRequest request, HttpServletResponse response)
throws IOException {
SavedRequest savedRequest = requestCache.getRequest(request, response);
if (savedRequest != null) {
String targetUrl = savedRequest.getRedirectUrl();
String loginPage = "/bw-login.html";
logger.info("引发跳转的请求是:" + targetUrl);
logger.info("引发跳转的请求是:" + loginPage);
if (StringUtils.endsWithIgnoreCase(targetUrl, ".html")) {
redirectStrategy.sendRedirect(request, response, loginPage);
}
}
return new R("访问的服务需要身份认证,请引导用户到登录页");
}
}
- 2.2 BrowserSecurityConfig
/**
* Web端security配置
*/
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.formLogin()
.loginPage("/authentication/require") // 当需要身份认证时,跳转到这里
.loginProcessingUrl("/authentication/form") // 默认处理的/login,自定义登录界面需要指定请求路径
.and()
.authorizeRequests()
.antMatchers(
"/bw-login.html",
"/authentication/require").permitAll()
.anyRequest()
.authenticated();
}
}
- 2.3 为了更加灵活,登录界面可以配置 封装配置.png
/**
* 系统配置类
*/
@Getter
@ConfigurationProperties("raven.security")
public class RavenSecurityProperties {
// 浏览器相关配置
private BrowserProperties browser = new BrowserProperties();
}
/**
* 浏览器相关配置类
*/
@Data
public class BrowserProperties {
private String loginPage = "/bw-login.html";
}
- 修改配置类
/**
* 当需要身份认证时,跳转到这里
*/
@RestController
public class BrowserSecurityController {
private Logger logger = LoggerFactory.getLogger(getClass());
// 获取session缓存
private RequestCache requestCache = new HttpSessionRequestCache();
// 跳转工具类
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Autowired
private RavenSecurityProperties securityProperties;
/**
* 当需要身份认证时,跳转到这里
*
* @param request
* @param response
* @return
* @throws IOException
*/
@RequestMapping("/authentication/require")
@ResponseStatus(code = HttpStatus.UNAUTHORIZED)
public R requireAuthentication(HttpServletRequest request, HttpServletResponse response)
throws IOException {
SavedRequest savedRequest = requestCache.getRequest(request, response);
if (savedRequest != null) {
String targetUrl = savedRequest.getRedirectUrl();
String loginPage = this.securityProperties.getBrowser().getLoginPage();
logger.info("引发跳转的请求是:" + targetUrl);
logger.info("引发跳转的请求是:" + loginPage);
if (StringUtils.endsWithIgnoreCase(targetUrl, ".html")) {
redirectStrategy.sendRedirect(request, response, loginPage);
}
}
return new R("访问的服务需要身份认证,请引导用户到登录页");
}
}
- 修改登录控制器
/**
* Web端security配置
*/
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private RavenSecurityProperties securityProperties;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 配置登录界面
String loginPage = this.securityProperties.getBrowser().getLoginPage();
http.csrf().disable()
.formLogin()
.loginPage("/authentication/require") // 当需要身份认证时,跳转到这里
.loginProcessingUrl("/authentication/form") // 默认处理的/login,自定义登录界面需要指定请求路径
.and()
.authorizeRequests()
.antMatchers("/authentication/require",
loginPage
).permitAll()
.anyRequest()
.authenticated();
}
}
- 2.4 Demo测试
- application.yml
raven:
security:
browser:
loginPage: /demo-login.html
四、自定义成功处理器
目前请求资源登录成功后会跳转到默认的界面或者是请求的资源页面上,为了更加灵活,需要自定义成功处理器来判断是直接返回成功页面还是返回JSON数据让用户自己决定如何处理
- BrowserAuthenticationSuccessHandler
@Component
public class BrowserAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private ObjectMapper objectMapper;
private Logger logger = LoggerFactory.getLogger(getClass());
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
logger.info("登录成功");
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(this.objectMapper.writeValueAsString(authentication));
}
}
- BrowserSecurityConfig添加处理成功的配置
/**
* Web端security配置
*/
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private RavenSecurityProperties securityProperties;
@Autowired
private BrowserAuthenticationSuccessHandler browserAuthenticationSuccessHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 配置登录界面
String loginPage = this.securityProperties.getBrowser().getLoginPage();
http.csrf().disable()
.formLogin()
.loginPage("/authentication/require") // 当需要身份认证时,跳转到这里
.loginProcessingUrl("/authentication/form") // 默认处理的/login,自定义登录界面需要指定请求路径
.successHandler(this.browserAuthenticationSuccessHandler)
.and()
.authorizeRequests()
.antMatchers("/authentication/require",
loginPage
).permitAll()
.anyRequest()
.authenticated();
}
}
成功处理器返回JSON格式数据.png
- 现在存在一个问题,那就是所有的成功都会返回JSON格式的数据,为了更加灵活,通过配置来决定成功后使返回JSON格式数据还是直接跳转到成功页面
- 添加配置
/**
* 浏览器相关配置类
*/
@Data
public class BrowserProperties {
private String loginPage = "/bw-login.html";
private RavenLoginType loginType = RavenLoginType.JSON;
}
- 成功处理器
@Component
public class BrowserAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private ObjectMapper objectMapper;
@Autowired
private RavenSecurityProperties securityProperties;
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
logger.info("登录成功处理器");
if (RavenLoginType.JSON.equals(this.securityProperties.getBrowser().getLoginType())) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(this.objectMapper.writeValueAsString(authentication));
}
else {
super.onAuthenticationSuccess(request, response, authentication);
}
}
}
- application.yml
raven:
security:
browser:
loginPage: /demo-login.html
loginType: REDIRECT
这样便可以配置返回JSON格式数据还是直接跳转,同理错误处理器也一样,代码如下
- 自定义失败处理器
@Component
public class BrowserAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private ObjectMapper objectMapper;
@Autowired
private RavenSecurityProperties securityProperties;
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
logger.info("自定义失败处理器");
if (RavenLoginType.JSON.equals(this.securityProperties.getBrowser().getLoginType())) {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(this.objectMapper.writeValueAsString(exception));
}
else {
super.onAuthenticationFailure(request, response, exception);
}
}
}
网友评论