Spring Security 怎么判断一个用户是否登录了呢?
非常的简单,只要 Spring Context 中存储了一个 Authentication 的对象,那么 Spring Security 就认为用户已经登录。所以所有的操作都是为了往 Spring Context 中存储了一个 Authentication 的对象,这样就实现了自动登录的功能:
1 2 3 User user = new User(...); Authentication auth = UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(auth);
下面的代码都是为了结合某些业务一起介绍的,写的很冗杂,不管怎么做,核心功能只要围绕上面这几行代码即可。
前面的实现可以使用表单进行登陆了,但是某些时候需要自动登录,例如使用 QQ 的第三方登录,服务器收到登陆成功的回调后,需要在我们的系统中继续使用本地账号登陆才行,这时就会需要实现自动登录的功能,还有使用 AJAX 等也不能使用表单登陆,也是需要调用登陆的接口才可以。
为了提供登陆的接口,需要实现 AuthenticationProvider 进行登陆,不再使用 Spring Security 的默认实现。当然实现了自动登录后,并不会影响 Spring Security 的表单登陆。
项目的文件如下,需要增加修改的文件有:
MyUserDetails.java
MyUserDetailsService.java
MyAuthenticationProvider.java
SecurityUtils.java
UserDao.java
AutoLoginController.java
spring-security.xml
login.fm
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 ├── java │ └── com │ └── xtuer │ ├── bean │ │ ├── User.java │ │ └── UserRole.java │ ├── controller │ │ ├── AutoLoginController.java │ │ ├── HelloController.java │ │ └── LoginController.java │ ├── dao │ │ └── UserDao.java │ ├── security │ │ ├── MyAuthenticationProvider.java │ │ ├── MyUserDetails.java │ │ ├── MyUserDetailsService.java │ │ └── SecurityUtils.java │ └── service ├── resources │ └── config │ ├── spring-mvc.xml │ └── spring-security.xml └── webapp └── WEB-INF ├── static ├── view │ ├── admin.fm │ ├── hello.fm │ └── login.fm └── web.xml
MyUserDetails.java 自定义的 UserDetails 作为 Principle 保存到 Spring Security 的登陆信息里,这样就可以使用自定义的 UserDetails 根据我们的业务规则保存足够的信息,默认的 Principle 只保存了用户名,绝大多数时候只有用户名是不够的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package com.xtuer.security;import com.xtuer.bean.User;import com.xtuer.bean.UserRole;import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.authority.SimpleGrantedAuthority;import java.util.ArrayList;import java.util.HashSet;import java.util.List;import java.util.Set;public class MyUserDetails extends org .springframework .security .core .userdetails .User { private int userId; public MyUserDetails (User user) { super (user.getUsername(), user.getPassword(), user.isEnabled(), true , true , true , buildUserAuthorities(user)); this .userId = user.getId(); } public int getUserId () { return userId; } public void setUserId (int userId) { this .userId = userId; } private static List<GrantedAuthority> buildUserAuthorities (User user) { Set<UserRole> userRoles = user.getUserRoles(); Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>(); for (UserRole userRole : userRoles) { authorities.add(new SimpleGrantedAuthority(userRole.getRole())); } return new ArrayList<GrantedAuthority>(authorities); } }
MyUserDetailsService.java 使用用户名从数据库查找到用户的信息,然后构建 UserDetails。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package com.xtuer.security;import com.xtuer.dao.UserDao;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UserDetailsService;public class MyUserDetailsService implements UserDetailsService { private UserDao userDao = new UserDao(); @Override public UserDetails loadUserByUsername (String username) { com.xtuer.bean.User user = userDao.findUserByUsername(username); if (user == null ) { System.out.println(username + " not found!" ); } return user == null ? null : new MyUserDetails(user); } }
The UserDetailsService
1 2 3 4 5 6 Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (principal instanceof UserDetails) { String username = ((UserDetails)principal).getUsername(); } else { String username = principal.toString(); }
Another item to note from the above code fragment is that you can obtain a principal from the Authentication object. The principal is just an Object. Most of the time this can be cast into a UserDetails object. UserDetails is a central interface in Spring Security. It represents a principal, but in an extensible and application-specific way. Think of UserDetails as the adapter between your own user database and what Spring Security needs inside the SecurityContextHolder. Being a representation of something from your own user database, quite often you will cast the UserDetails to the original object that your application provided, so you can call business-specific methods (like getEmail(), getEmployeeNumber() and so on).
By now you’re probably wondering, so when do I provide a UserDetails object? How do I do that? I thought you said this thing was declarative and I didn’t need to write any Java code - what gives? The short answer is that there is a special interface called UserDetailsService. The only method on this interface accepts a String-based username argument and returns a UserDetails:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; This is the most common approach to loading information for a user within Spring Security and you will see it used throughout the framework whenever information on a user is required.
On successful authentication, UserDetails is used to build the Authentication object that is stored in the SecurityContextHolder (more on this below). The good news is that we provide a number of UserDetailsService implementations, including one that uses an in-memory map (InMemoryDaoImpl) and another that uses JDBC (JdbcDaoImpl). Most users tend to write their own, though, with their implementations often simply sitting on top of an existing Data Access Object (DAO) that represents their employees, customers, or other users of the application. Remember the advantage that whatever your UserDetailsService returns can always be obtained from the SecurityContextHolder using the above code fragment.
MyAuthenticationProvider.java MyAuthenticationProvider 提供了登陆的逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 package com.xtuer.security;import org.springframework.security.authentication.*;import org.springframework.security.core.Authentication;import org.springframework.security.core.AuthenticationException;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UsernameNotFoundException;import org.springframework.security.crypto.password.PasswordEncoder;import javax.annotation.Resource;public class MyAuthenticationProvider implements AuthenticationProvider { @Resource(name="userDetailsService") private MyUserDetailsService userDetailsService; @Resource(name="passwordEncoder") private PasswordEncoder passwordEncoder; @Override public Authentication authenticate (Authentication authentication) throws AuthenticationException { UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication; UserDetails userDetails = userDetailsService.loadUserByUsername(token.getName()); if (userDetails == null ) { throw new UsernameNotFoundException("用户不存在" ); } else if (!userDetails.isEnabled()){ throw new DisabledException("用户已被禁用" ); } else if (!userDetails.isAccountNonExpired()) { throw new AccountExpiredException("账号已过期" ); } else if (!userDetails.isAccountNonLocked()) { throw new LockedException("账号已被锁定" ); } else if (!userDetails.isCredentialsNonExpired()) { throw new LockedException("凭证已过期" ); } if (isOAuthUser(userDetails)) { } else { String encryptedPassword = userDetails.getPassword(); String inputPassword = (String) token.getCredentials(); if (!passwordEncoder.matches(inputPassword, encryptedPassword)) { throw new BadCredentialsException("用户名/密码无效" ); } } return new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities()); } @Override public boolean supports (Class<?> authentication) { return UsernamePasswordAuthenticationToken.class.equals(authentication); } private boolean isOAuthUser (UserDetails userDetails) { return userDetails.getUsername().startsWith("QQ_" ); } }
SecurityUtils.java SecurityUtils 提供了登陆的接口 login()
,还有一些和登陆有关的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 package com.xtuer.security;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;import org.springframework.security.core.Authentication;import org.springframework.security.core.context.SecurityContextHolder;import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices;import org.springframework.security.web.savedrequest.HttpSessionRequestCache;import org.springframework.security.web.savedrequest.SavedRequest;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.context.request.ServletRequestAttributes;import javax.annotation.Resource;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;public class SecurityUtils { @Resource(name = "authenticationManager") private AuthenticationManager authenticationManager; @Autowired private TokenBasedRememberMeServices tokenBasedRememberMeServices; public static boolean isLogin () { String username = SecurityContextHolder.getContext().getAuthentication().getName(); return !"anonymousUser" .equals(username); } public static int getLoginUserId () { Object principle = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (SecurityUtils.isLogin()) { MyUserDetails userDetails = (MyUserDetails) principle; return userDetails.getUserId(); } return -1 ; } public String login (String username, String password) { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse(); String defaultTargetUrl = "/" ; String redirectUrl = "/login?error=1" ; try { Authentication token = new UsernamePasswordAuthenticationToken(username, password); token = authenticationManager.authenticate(token); SecurityContextHolder.getContext().setAuthentication(token); tokenBasedRememberMeServices.onLoginSuccess(request, response, token); SavedRequest savedRequest = new HttpSessionRequestCache().getRequest(request, response); redirectUrl = (savedRequest != null ) ? savedRequest.getRedirectUrl() : defaultTargetUrl; } catch (Exception ex) { System.out.println(ex.getMessage()); } return redirectUrl; } }
UserDao.java 增加了 QQ 用户 QQ_Admin,用户名的前缀为 QQ_,只是为了模拟第三方登录后需要使用本地账号自动登录时使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package com.xtuer.dao;import com.xtuer.bean.User;import com.xtuer.bean.UserRole;import java.util.Arrays;import java.util.HashMap;import java.util.HashSet;import java.util.Map;public class UserDao { private static Map<String, User> users = new HashMap<String, User>(); static { UserRole userRole = new UserRole("ROLE_USER" ); UserRole adminRole = new UserRole("ROLE_ADMIN" ); String password = "$2a$10$gtaxGaHMfxMRj6rqK/kp0.5TPF13CBvnXhvD7teUmeftH1cX0Mb6S" ; users.put("admin" , new User("admin" , password, true , new HashSet<UserRole>(Arrays.asList(adminRole)))); users.put("alice" , new User("alice" , password, true , new HashSet<UserRole>(Arrays.asList(userRole)))); users.put("QQ_admin" , new User("QQ_admin" , password, true , new HashSet<UserRole>(Arrays.asList(adminRole)))); } public User findUserByUsername (String username) { return users.get(username); } }
AutoLoginController.java 在 AutoLoginController 中实现列举了第三方登录后绑定本地用户,然后自动登录的逻辑,使用登陆接口进行登陆,AJAX 实现登陆时就可以用到,还有不存在的用户登陆。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 package com.xtuer.controller;import com.xtuer.security.SecurityUtils;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.GetMapping;import javax.annotation.Resource;@Controller public class AutoLoginController { @Resource(name="securityUtils") private SecurityUtils securityUtils; @GetMapping("/bindingUser") public String bindingUser () { String username = "QQ_admin" ; String password = "wrong" ; return "redirect:" + securityUtils.login(username, password); } @GetMapping("/nonExistingUserLogin") public String nonExistingUserLogin () { String username = "nonExistingUser" ; String password = "flash" ; return "redirect:" + securityUtils.login(username, password); } @GetMapping("/adminLogin") public String adminLogin () { String username = "admin" ; String password = "Passw0rd" ; return "redirect:" + securityUtils.login(username, password); } }
spring-security.xml 需要注意的是和以前相比较,authentication-manager 的配置有变化,把密码加密部分去掉了,密码匹配的实现在 MyAuthenticationProvider 中使用。还因为使用了 @Resource, @Autowired 自动装配功能,所以需要把 <context:annotation-config/>
添加到配置中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 <?xml version="1.0" encoding="UTF-8"?> <beans:beans xmlns ="http://www.springframework.org/schema/security" xmlns:beans ="http://www.springframework.org/schema/beans" xmlns:context ="http://www.springframework.org/schema/context" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd" > <context:annotation-config /> <http auto-config ="true" > <intercept-url pattern ="/login" access ="permitAll" /> <intercept-url pattern ="/admin" access ="hasRole('ADMIN')" /> <form-login login-page ="/login" login-processing-url ="/login" default-target-url ="/hello" authentication-failure-url ="/login?error=1" username-parameter ="username" password-parameter ="password" /> <access-denied-handler error-page ="/deny" /> <logout logout-url ="/logout" logout-success-url ="/login?logout=1" /> <csrf disabled ="true" /> <remember-me key ="uniqueAndSecret" token-validity-seconds ="2592000" /> </http > <beans:bean id ="securityUtils" class ="com.xtuer.security.SecurityUtils" /> <beans:bean id ="passwordEncoder" class ="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" /> <beans:bean id ="userDetailsService" class ="com.xtuer.security.MyUserDetailsService" /> <beans:bean id ="authenticationProvider" class ="com.xtuer.security.MyAuthenticationProvider" /> <authentication-manager alias ="authenticationManager" > <authentication-provider ref ="authenticationProvider" /> </authentication-manager > </beans:beans >
login.fm 增加了几种不同的登陆链接。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <html > <head > <title > Login Page</title > </head > <body > ${error!}${logout!} <form name ="loginForm" action ="/login" method ="POST" > Username: <input type ="text" name ="username" /> <br > Password: <input type ="password" name ="password" /> <br > <input type ="checkbox" name ="remember-me" /> Remember Me<br > <input name ="submit" type ="submit" value ="登陆" /> </form > <a href ="/bindingUser" > QQ 绑定用户自动登陆</a > <br > <a href ="/adminLogin" > Admin 登陆</a > <br > <a href ="/nonExistingUserLogin" > 不存在用户登陆</a > </body > </html >
测试
访问 http://localhost:8080/admin ,重定向到登陆页面
使用表单登陆,功能和以前一样
注销,访问 http://localhost:8080/admin ,重定向到登陆页面
点击 QQ 绑定用户自动登陆
,登陆成功后重定向到 Admin 页面
注销,访问 http://localhost:8080/admin ,重定向到登陆页面
点击,Admin 登陆
,登陆成功后重定向到 Admin 页面
注销,访问 http://localhost:8080/admin ,重定向到登陆页面
点击 不存在用户登陆
,提示登陆失败
项目下载 由于项目的内容比较多,所以可以下载到开发环境里测试: spring-security.7z